diff --git a/assets/js/src/actions.js b/assets/js/src/actions.js index 22d80e7c..502e4473 100644 --- a/assets/js/src/actions.js +++ b/assets/js/src/actions.js @@ -1,75 +1,198 @@ -import { addAction } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; + import { NAMESPACE, ACTION_PREFIX } from './constants'; import { + trackListProducts, + trackAddToCart, + trackChangeCartItemQuantity, + trackRemoveCartItem, + trackCheckoutStep, + trackCheckoutOption, trackEvent, - getProductFieldObject, - getProductImpressionObject, -} from './utils'; - -const trackListProducts = ( { - products, - listName = __( 'Product List', 'woocommerce-google-analytics-integration' ), -} ) => { - trackEvent( 'view_item_list', { - event_category: 'engagement', - event_label: __( - 'Viewing products', - 'woocommerce-google-analytics-integration' - ), - items: products.map( ( product, index ) => ( { - ...getProductImpressionObject( product, listName ), - list_position: index + 1, - } ) ), - } ); -}; + trackSelectContent, + trackSearch, + trackViewItem, + trackException, +} from './tracking'; +import { addUniqueAction } from './utils'; -const trackAddToCart = ( { product, quantity = 1 } ) => { - trackEvent( 'add_to_cart', { - event_category: 'ecommerce', - event_label: __( - 'Add to Cart', - 'woocommerce-google-analytics-integration' - ), - items: [ getProductFieldObject( product, quantity ) ], - } ); -}; +/** + * Track customer progress through steps of the checkout. Triggers the event when the step changes: + * 1 - Contact information + * 2 - Shipping address + * 3 - Billing address + * 4 - Shipping options + * 5 - Payment options + * + * @summary Track checkout progress with begin_checkout and checkout_progress + * @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#1_measure_checkout_steps + */ +addUniqueAction( + `${ ACTION_PREFIX }-checkout-render-checkout-form`, + NAMESPACE, + ( { ...storeCart } ) => trackCheckoutStep( 0 )( storeCart ) +); +addUniqueAction( + `${ ACTION_PREFIX }-checkout-set-email-address`, + NAMESPACE, + ( { ...storeCart } ) => trackCheckoutStep( 1 )( storeCart ) +); +addUniqueAction( + `${ ACTION_PREFIX }-checkout-set-shipping-address`, + NAMESPACE, + ( { ...storeCart } ) => trackCheckoutStep( 2 )( storeCart ) +); +addUniqueAction( + `${ ACTION_PREFIX }-checkout-set-billing-address`, + NAMESPACE, + ( { ...storeCart } ) => trackCheckoutStep( 3 )( storeCart ) +); +addUniqueAction( + `${ ACTION_PREFIX }-checkout-set-phone-number`, + NAMESPACE, + ( { step, ...storeCart } ) => { + trackCheckoutStep( step === 'shipping' ? 2 : 3 )( storeCart ); + } +); -const trackRemoveCartItem = ( { product, quantity = 1 } ) => { - trackEvent( 'remove_from_cart', { - event_category: 'ecommerce', - event_label: __( - 'Remove Cart Item', - 'woocommerce-google-analytics-integration' - ), - items: [ getProductFieldObject( product, quantity ) ], - } ); -}; +/** + * Choose a shipping rate + * + * @summary Track the shipping rate being set using set_checkout_option + * @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options + */ +addUniqueAction( + `${ ACTION_PREFIX }-checkout-set-selected-shipping-rate`, + NAMESPACE, + ( { shippingRateId } ) => { + trackCheckoutOption( { + step: 4, + option: __( 'Shipping Method', 'woo-gutenberg-products-block' ), + value: shippingRateId, + } )(); + } +); -const trackChangeCartItemQuantity = ( { product, quantity = 1 } ) => { - trackEvent( 'change_cart_quantity', { - event_category: 'ecommerce', - event_label: __( - 'Change Cart Item Quantity', - 'woocommerce-google-analytics-integration' - ), - items: [ getProductFieldObject( product, quantity ) ], - } ); -}; +/** + * Choose a payment method + * + * @summary Track the payment method being set using set_checkout_option + * @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options + */ +addUniqueAction( + `${ ACTION_PREFIX }-checkout-set-active-payment-method`, + NAMESPACE, + ( { paymentMethodSlug } ) => { + trackCheckoutOption( { + step: 5, + option: __( 'Payment Method', 'woo-gutenberg-products-block' ), + value: paymentMethodSlug, + } )(); + } +); -addAction( +/** + * Product List View + * + * @summary Track the view_item_list event + * @see https://developers.google.com/gtagjs/reference/ga4-events#view_item_list + */ +addUniqueAction( `${ ACTION_PREFIX }-product-list-render`, NAMESPACE, trackListProducts ); -addAction( `${ ACTION_PREFIX }-cart-add-item`, NAMESPACE, trackAddToCart ); -addAction( + +/** + * Add to cart. + * + * This event signifies that an item was added to a cart for purchase. + * + * @summary Track the add_to_cart event + * @see https://developers.google.com/gtagjs/reference/ga4-events#add_to_cart + */ +addUniqueAction( + `${ ACTION_PREFIX }-cart-add-item`, + NAMESPACE, + trackAddToCart +); + +/** + * Change cart item quantities + * + * @summary Custom change_cart_quantity event. + */ +addUniqueAction( `${ ACTION_PREFIX }-cart-set-item-quantity`, NAMESPACE, trackChangeCartItemQuantity ); -addAction( + +/** + * Remove item from the cart + * + * @summary Track the remove_from_cart event + * @see https://developers.google.com/gtagjs/reference/ga4-events#remove_from_cart + */ +addUniqueAction( `${ ACTION_PREFIX }-cart-remove-item`, NAMESPACE, trackRemoveCartItem ); + +/** + * Add Payment Information + * + * This event signifies a user has submitted their payment information. Note, this is used to indicate checkout + * submission, not `purchase` which is triggered on the thanks page. + * + * @summary Track the add_payment_info event + * @see https://developers.google.com/gtagjs/reference/ga4-events#add_payment_info + */ +addUniqueAction( `${ ACTION_PREFIX }-checkout-submit`, NAMESPACE, () => { + trackEvent( 'add_payment_info' ); +} ); + +/** + * Product View Link Clicked + * + * @summary Track the select_content event + * @see https://developers.google.com/gtagjs/reference/ga4-events#select_content + */ +addUniqueAction( + `${ ACTION_PREFIX }-product-view-link`, + NAMESPACE, + trackSelectContent +); + +/** + * Product Search + * + * @summary Track the search event + * @see https://developers.google.com/gtagjs/reference/ga4-events#search + */ +addUniqueAction( `${ ACTION_PREFIX }-product-search`, NAMESPACE, trackSearch ); + +/** + * Single Product View + * + * @summary Track the view_item event + * @see https://developers.google.com/gtagjs/reference/ga4-events#view_item + */ +addUniqueAction( + `${ ACTION_PREFIX }-product-render`, + NAMESPACE, + trackViewItem +); + +/** + * Track notices as Exception events. + * + * @summary Track the exception event + * @see https://developers.google.com/analytics/devguides/collection/gtagjs/exceptions + */ +addUniqueAction( + `${ ACTION_PREFIX }-store-notice-create`, + NAMESPACE, + trackException +); diff --git a/assets/js/src/constants.js b/assets/js/src/constants.js index 2a6b82c0..0641ff80 100644 --- a/assets/js/src/constants.js +++ b/assets/js/src/constants.js @@ -1,2 +1,2 @@ -export const NAMESPACE = 'woocommerce-google-analytics-integration'; +export const NAMESPACE = 'woocommerce-google-analytics'; export const ACTION_PREFIX = 'experimental__woocommerce_blocks'; diff --git a/assets/js/src/tracking.js b/assets/js/src/tracking.js new file mode 100644 index 00000000..63329121 --- /dev/null +++ b/assets/js/src/tracking.js @@ -0,0 +1,219 @@ +import { __ } from '@wordpress/i18n'; +import { + getProductFieldObject, + getProductImpressionObject, + formatPrice, +} from './utils'; + +/** + * Variable holding the current checkout step. It will be modified by trackCheckoutOption and trackCheckoutStep methods. + * + * @type {number} + */ +let currentStep = -1; + +/** + * Tracks view_item_list event + * + * @param {Object} params The function params + * @param {Array} params.products The products to track + * @param {string} [params.listName] The name of the list in which the item was presented to the user. + */ +export const trackListProducts = ( { + products, + listName = __( 'Product List', 'woocommerce-google-analytics-integration' ), +} ) => { + trackEvent( 'view_item_list', { + event_category: 'engagement', + event_label: __( + 'Viewing products', + 'woocommerce-google-analytics-integration' + ), + items: products.map( ( product, index ) => ( { + ...getProductImpressionObject( product, listName ), + list_position: index + 1, + } ) ), + } ); +}; + +/** + * Tracks add_to_cart event + * + * @param {Object} params The function params + * @param {Array} params.product The product to track + * @param {number} [params.quantity=1] The quantity of that product in the cart. + */ +export const trackAddToCart = ( { product, quantity = 1 } ) => { + trackEvent( 'add_to_cart', { + event_category: 'ecommerce', + event_label: __( + 'Add to Cart', + 'woocommerce-google-analytics-integration' + ), + items: [ getProductFieldObject( product, quantity ) ], + } ); +}; + +/** + * Tracks remove_from_cart event + * + * @param {Object} params The function params + * @param {Array} params.product The product to track + * @param {number} [params.quantity=1] The quantity of that product in the cart. + */ +export const trackRemoveCartItem = ( { product, quantity = 1 } ) => { + trackEvent( 'remove_from_cart', { + event_category: 'ecommerce', + event_label: __( + 'Remove Cart Item', + 'woocommerce-google-analytics-integration' + ), + items: [ getProductFieldObject( product, quantity ) ], + } ); +}; + +/** + * Tracks change_cart_quantity event + * + * @param {Object} params The function params + * @param {Array} params.product The product to track + * @param {number} [params.quantity=1] The quantity of that product in the cart. + */ +export const trackChangeCartItemQuantity = ( { product, quantity = 1 } ) => { + trackEvent( 'change_cart_quantity', { + event_category: 'ecommerce', + event_label: __( + 'Change Cart Item Quantity', + 'woocommerce-google-analytics-integration' + ), + items: [ getProductFieldObject( product, quantity ) ], + } ); +}; + +/** + * Track a begin_checkout and checkout_progress event + * Notice calling this will set the current checkout step as the step provided in the parameter. + * + * @param {number} step The checkout step for to track + * @return {(function( { storeCart: Object } ): void)} A callable receiving the cart to track the checkout event. + */ +export const trackCheckoutStep = + ( step ) => + ( { storeCart } ) => { + if ( currentStep === step ) { + return; + } + + trackEvent( step === 0 ? 'begin_checkout' : 'checkout_progress', { + items: storeCart.cartItems.map( getProductFieldObject ), + coupon: storeCart.cartCoupons[ 0 ]?.code || '', + currency: storeCart.cartTotals.currency_code, + value: formatPrice( + storeCart.cartTotals.total_price, + storeCart.cartTotals.currency_minor_unit + ), + checkout_step: step, + } ); + + currentStep = step; + }; + +/** + * Track a set_checkout_option event + * Notice calling this will set the current checkout step as the step provided in the parameter. + * + * @param {Object} params The params from the option. + * @param {number} params.step The step to track + * @param {string} params.option The option to set in checkout + * @param {string} params.value The value for the option + * + * @return {(function() : void)} A callable to track the checkout event. + */ +export const trackCheckoutOption = + ( { step, option, value } ) => + () => { + trackEvent( 'set_checkout_option', { + checkout_step: step, + checkout_option: option, + value, + } ); + + currentStep = step; + }; + +/** + * Tracks select_content event. + * + * @param {Object} params The function params + * @param {Object} params.product The product to track + * @param {string} params.listName The name of the list in which the item was presented to the user. + */ +export const trackSelectContent = ( { + product, + listName = __( 'Product List', 'woocommerce-google-analytics-integration' ), +} ) => { + trackEvent( 'select_content', { + content_type: 'product', + items: [ getProductImpressionObject( product, listName ) ], + } ); +}; + +/** + * Tracks search event. + * + * @param {Object} params The function params + * @param {string} params.searchTerm The search term to track + */ +export const trackSearch = ( { searchTerm } ) => { + trackEvent( 'search', { + search_term: searchTerm, + } ); +}; + +/** + * Tracks view_item event + * + * @param {Object} params The function params + * @param {Object} params.product The product to track + * @param {string} [params.listName] The name of the list in which the item was presented to the user. + */ +export const trackViewItem = ( { + product, + listName = __( 'Product List', 'woocommerce-google-analytics-integration' ), +} ) => { + if ( product ) { + trackEvent( 'view_item', { + items: [ getProductImpressionObject( product, listName ) ], + } ); + } +}; + +/** + * Track exception event + * + * @param {Object} params The function params + * @param {string} params.status The status of the exception. It should be "error" for tracking it. + * @param {string} params.content The exception description + */ +export const trackException = ( { status, content } ) => { + if ( status === 'error' ) { + trackEvent( 'exception', { + description: content, + fatal: false, + } ); + } +}; + +/** + * Track an event using the global gtag function. + * + * @param {string} eventName - Name of the event to track + * @param {Object} [eventParams] - Props to send within the event + */ +export const trackEvent = ( eventName, eventParams ) => { + if ( typeof gtag !== 'function' ) { + throw new Error( 'Function gtag not implemented.' ); + } + + window.gtag( 'event', eventName, eventParams ); +}; diff --git a/assets/js/src/utils.js b/assets/js/src/utils.js index c2126098..af74dfda 100644 --- a/assets/js/src/utils.js +++ b/assets/js/src/utils.js @@ -1,16 +1,4 @@ -/** - * Track an event using the global gtag function. - * - * @param {string} eventName - Name of the event to track - * @param {Object} eventParams - Props to send within the event - */ -export const trackEvent = ( eventName, eventParams ) => { - if ( typeof gtag !== 'function' ) { - throw new Error( 'Function gtag not implemented.' ); - } - - window.gtag( 'event', eventName, eventParams ); -}; +import { addAction, removeAction } from '@wordpress/hooks'; /** * Formats data into the productFieldObject shape. @@ -27,7 +15,10 @@ export const getProductFieldObject = ( product, quantity ) => { name: product.name, quantity, category: getProductCategory( product ), - price: getPrice( product ), + price: formatPrice( + product.prices.price, + product.prices.currency_minor_unit + ), }; }; @@ -46,10 +37,37 @@ export const getProductImpressionObject = ( product, listName ) => { name: product.name, list_name: listName, category: getProductCategory( product ), - price: getPrice( product ), + price: formatPrice( + product.prices.price, + product.prices.currency_minor_unit + ), }; }; +/** + * Returns the price of a product formatted as a string. + * + * @param {string} price - The price to parse + * @param {number} [currencyMinorUnit=2] - The number decimals to show in the currency + * + * @return {string} - The price of the product formatted + */ +export const formatPrice = ( price, currencyMinorUnit = 2 ) => { + return ( parseInt( price, 10 ) / 10 ** currencyMinorUnit ).toString(); +}; + +/** + * Removes previous actions with the same hookName and namespace and then adds the new action. + * + * @param {string} hookName The hook name for the action + * @param {string} namespace The unique namespace for the action + * @param {Function} callback The function to run when the action happens. + */ +export const addUniqueAction = ( hookName, namespace, callback ) => { + removeAction( hookName, namespace ); + addAction( hookName, namespace, callback ); +}; + /** * Returns the product ID by checking if the product has a SKU, if not, it returns '#' concatenated with the product ID. * @@ -73,17 +91,3 @@ const getProductCategory = ( product ) => { ? product.categories[ 0 ].name : ''; }; - -/** - * Returns the price of a product as a string. - * - * @param {Object} product - The product object - * - * @return {string} - The price of the product - */ -const getPrice = ( product ) => { - return ( - parseInt( product.prices.price, 10 ) / - 10 ** product.prices.currency_minor_unit - ).toString(); -}; diff --git a/package.json b/package.json index 2d32eff6..05105ccb 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "lint:php:diff": "./bin/phpcs-diff.sh", "archive": "composer archive --file=$npm_package_name --format=zip", "postarchive": "rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name", - "build": "NODE_ENV=production wp-scripts build && npm run makepot && npm run archive", - "prebuild": "rm -rf ./vendor" + "build": "NODE_ENV=production wp-scripts build && npm run makepot && npm run archive", + "prebuild": "rm -rf ./vendor" }, "engines": { "node": ">=8.9.3",