Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate the global site tag setup and event tracking #398

Merged
merged 26 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c1cc257
Setup global site tag in head
martynmjones Mar 13, 2024
0a3af1e
Wait until DOMContentLoaded before tracking
martynmjones Mar 13, 2024
ac06b6d
CS
martynmjones Mar 13, 2024
d064dfb
Initialize instantly, if the document is already loaded.
tomalec Mar 13, 2024
f3fe7b4
Add code doc explaining why we wait to add block actions
tomalec Mar 13, 2024
9ed093f
Enqueue tagmanager in regular way,
tomalec Mar 13, 2024
5a9d161
Re-add `identifier` to the settings object
tomalec Mar 13, 2024
92e96fb
Bring back the `woocommerce_ga_gtag_config` hook.
tomalec Mar 13, 2024
c241828
Bring back `trackClassicPages` params to limit the number of changes
tomalec Mar 13, 2024
9fede11
Rewrite tracker to more functional structure,
tomalec Mar 14, 2024
75e3053
Separate data from settings in JS object
tomalec Mar 19, 2024
f2afcf3
Remove `woocommerce-google-analytics-integration-js-before` assertion…
tomalec Mar 19, 2024
ae7562d
Harden no-trackign for admin tests
tomalec Mar 19, 2024
8e7481d
Add E2E tests to confirm tracking works regardless of where the scrip…
martynmjones Mar 22, 2024
25bd09c
Update related products add to cart selector
martynmjones Mar 22, 2024
582e41b
Wrap tag setup in wp_print_inline_script_tag
martynmjones Mar 22, 2024
d1629c2
CS fixes
martynmjones Mar 22, 2024
4d6f371
Fix the convention for JS block comments
tomalec Mar 26, 2024
cdb60c0
Rename event handler-related function names
tomalec Mar 26, 2024
867f696
Unify `self::` to `$this->` reference in instance methods
tomalec Mar 27, 2024
89487e6
Use `static::` to reference `DEVELOPER_ID` in parent class,
tomalec Mar 27, 2024
dacaabc
Use the data if ready, wait for our own event to use it,
tomalec Mar 28, 2024
77067d8
Move gtag and dataLayer setup to registered inline script
martynmjones Apr 3, 2024
f154988
Merge branch 'trunk' into update/separate-site-tag-setup-and-event-tr…
martynmjones Apr 4, 2024
757b3e8
PHPCS and add snippet to bypass WooCommerce dependency in e2e tests
Apr 4, 2024
5d587a3
Fix WC bypass snippet
Apr 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions assets/js/src/config.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/* global wcgai */
export const config = wcgai.config;
export const config = () => {
return window.ga4wData; // esling-disable-line no-unused-vars
};
27 changes: 22 additions & 5 deletions assets/js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// Initialize tracking for classic WooCommerce pages
import { trackClassicPages } from './integrations/classic';
window.wcgai.trackClassicPages = trackClassicPages;
import { config } from './config';
import { classicTracking } from './integrations/classic';
import { blocksTracking } from './integrations/blocks';

// Initialize tracking for Block based WooCommerce pages
import './integrations/blocks';
// Wait for DOMContentLoaded to make sure event data is in place.
if ( document.readyState === 'loading' ) {
document.addEventListener(
tomalec marked this conversation as resolved.
Show resolved Hide resolved
'DOMContentLoaded',
eventuallyInitializeTracking
);
} else {
eventuallyInitializeTracking();
}
function eventuallyInitializeTracking() {
if ( ! config() ) {
throw new Error(
'Google Analytics for WooCommerce: Configuration and tracking data not found.'
);
}

classicTracking( config() );
blocksTracking();
}
71 changes: 37 additions & 34 deletions assets/js/src/integrations/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,49 @@ import { addUniqueAction } from '../utils';
import { tracker } from '../tracker';
import { ACTION_PREFIX, NAMESPACE } from '../constants';

addUniqueAction(
`${ ACTION_PREFIX }-product-render`,
NAMESPACE,
tracker.eventHandler( 'view_item' )
);
// We add actions asynchronosly, to make sure handlares will have the config available.
export const blocksTracking = () => {
addUniqueAction(
`${ ACTION_PREFIX }-product-render`,
NAMESPACE,
tracker.eventHandler( 'view_item' )
);

addUniqueAction(
`${ ACTION_PREFIX }-cart-remove-item`,
NAMESPACE,
tracker.eventHandler( 'remove_from_cart' )
);
addUniqueAction(
`${ ACTION_PREFIX }-cart-remove-item`,
NAMESPACE,
tracker.eventHandler( 'remove_from_cart' )
);

addUniqueAction(
`${ ACTION_PREFIX }-checkout-render-checkout-form`,
NAMESPACE,
tracker.eventHandler( 'begin_checkout' )
);
addUniqueAction(
`${ ACTION_PREFIX }-checkout-render-checkout-form`,
NAMESPACE,
tracker.eventHandler( 'begin_checkout' )
);

// These actions only works for All Products Block
addUniqueAction(
`${ ACTION_PREFIX }-cart-add-item`,
NAMESPACE,
( { product } ) => {
tracker.eventHandler( 'add_to_cart' )( { product } );
}
);
// These actions only works for All Products Block
addUniqueAction(
`${ ACTION_PREFIX }-cart-add-item`,
NAMESPACE,
( { product } ) => {
tracker.eventHandler( 'add_to_cart' )( { product } );
}
);

addUniqueAction(
`${ ACTION_PREFIX }-product-list-render`,
NAMESPACE,
tracker.eventHandler( 'view_item_list' )
);
addUniqueAction(
`${ ACTION_PREFIX }-product-list-render`,
NAMESPACE,
tracker.eventHandler( 'view_item_list' )
);

addUniqueAction(
`${ ACTION_PREFIX }-product-view-link`,
NAMESPACE,
tracker.eventHandler( 'select_content' )
);
addUniqueAction(
`${ ACTION_PREFIX }-product-view-link`,
NAMESPACE,
tracker.eventHandler( 'select_content' )
);
};

/**
/*
* Remove additional actions added by WooCommerce Core which are either
* not supported by Google Analytics for WooCommerce or are redundant
* since Google retired Universal Analytics.
Expand Down
15 changes: 7 additions & 8 deletions assets/js/src/integrations/classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { getProductFromID } from '../utils';
* @param {Object} data.added_to_cart - The product added to cart.
* @param {Object} data.order - The order object.
*/
export function trackClassicPages( {
export function classicTracking( {
events,
cart,
products,
Expand All @@ -30,17 +30,16 @@ export function trackClassicPages( {
order,
} ) {
// Instantly track the events listed in the `events` object.
const eventData = {
storeCart: cart,
products,
product,
order,
};
Object.values( events ?? {} ).forEach( ( eventName ) => {
if ( eventName === 'add_to_cart' ) {
tracker.eventHandler( eventName )( { product: addedToCart } );
} else {
tracker.eventHandler( eventName )( eventData );
tracker.eventHandler( eventName )( {
storeCart: cart,
products,
product,
order,
} );
}
} );

Expand Down
34 changes: 8 additions & 26 deletions assets/js/src/tracker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,6 @@ class Tracker {
throw new Error( 'Cannot instantiate more than one Tracker' );
}
instance = this;

window.dataLayer = window.dataLayer || [];

function gtag() {
window.dataLayer.push( arguments );
}

window[ config.tracker_function_name ] = gtag;

// Set up default consent state, denying all for EEA visitors.
for ( const mode of config.consent_modes || [] ) {
gtag( 'consent', 'default', mode );
}

gtag( 'js', new Date() );
gtag( 'set', `developer_id.${ config.developer_id }`, true );
gtag( 'config', config.gtag_id, {
allow_google_signals: config.allow_google_signals,
link_attribution: config.link_attribution,
anonymize_ip: config.anonymize_ip,
logged_in: config.logged_in,
linker: config.linker,
custom_map: config.custom_map,
} );
}

/**
Expand All @@ -53,6 +29,12 @@ class Tracker {
* @throws {Error} If the event name is not supported.
*/
eventHandler( name ) {
if ( ! config() ) {
throw new Error(
'Google Analytics for WooCommerce: eventHandler called too early'
);
}

/* eslint import/namespace: [ 'error', { allowComputed: true } ] */
const formatter = formatters[ name ];
if ( typeof formatter !== 'function' ) {
Expand All @@ -61,8 +43,8 @@ class Tracker {

return function trackerEventHandler( data ) {
const eventData = formatter( data );
if ( config.events.includes( name ) && eventData ) {
window[ config.tracker_function_name ](
if ( config().settings.events.includes( name ) && eventData ) {
window[ config().settings.tracker_function_name ](
'event',
name,
eventData
Expand Down
2 changes: 1 addition & 1 deletion assets/js/src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const getProductId = ( product ) => {
return identifier;
}

if ( config.identifier === 'product_sku' ) {
if ( config().settings.identifier === 'product_sku' ) {
return product.sku ? product.sku : '#' + product.id;
}

Expand Down
91 changes: 56 additions & 35 deletions includes/class-wc-google-gtag-js.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,40 @@ public function __construct( $settings = array() ) {
$this->map_hooks();

// Setup frontend scripts
add_action( 'wp_head', array( $this, 'setup_site_tag' ), 2 );
add_action( 'wp_enqueue_scripts', array( $this, 'enquque_tracker' ), 5 );
add_action( 'wp_footer', array( $this, 'inline_script_data' ) );
}

/**
* Setup the global site tag as early as possible on the page
*
* @return void
*/
public function setup_site_tag() {
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
printf(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should be so brutal to break WP rules and print script directly; maybe enqueueing it as an inline with/ no dependencies would be enough? Or does the "optimization" plugins could force scripts enqueued in the head to be loaded later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of those plugins have settings to combine all inline JS and some will move them to a different file. As we know the script needs to be available at this point no matter what I think it makes sense that we avoid that possibility.

Instead of outputting it directly I've wrapped it in wp_print_inline_script_tag so that's we're still doing things the WordPress way 582e41b

'<!-- Google Analytics for WooCommerce (gtag.js) -->
<script>
window.dataLayer = window.dataLayer || [];
function %2$s(){dataLayer.push(arguments);}
// Set up default consent state.
for ( const mode of %4$s || [] ) {
%2$s( "consent", "default", mode );
}
%2$s("js", new Date());
%2$s("set", "developer_id.%3$s", true);
%2$s("config", "%1$s", %5$s);
</script>',
esc_js( self::get( 'ga_id' ) ),
esc_js( self::tracker_function_name() ),
esc_js( self::DEVELOPER_ID ),
json_encode( self::get_consent_modes() ),
json_encode( $this->get_site_tag_config() )
);
// phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
}

/**
* Register tracker scripts and its inline config.
* We need to execute tracker.js w/ `gtag` configuration before any trackable action may happen.
Expand All @@ -69,8 +99,7 @@ public function enquque_tracker(): void {
'strategy' => 'async',
)
);
// tracker.js needs to be executed ASAP, the remaining bits for main.js could be deffered,
// but to reduce the traffic, we ship it all together.

wp_enqueue_script(
$this->script_handle,
Plugin::get_instance()->get_js_asset_url( 'main.js' ),
Expand All @@ -81,24 +110,23 @@ public function enquque_tracker(): void {
Plugin::get_instance()->get_js_asset_version( 'main' ),
true
);
// Provide tracker's configuration.
wp_add_inline_script(
$this->script_handle,
sprintf(
'var wcgai = {config: %s};',
wp_json_encode( $this->get_analytics_config() )
),
'before'
);
}

/**
* Feed classic tracking with event data via inline script.
* Make sure it's added at the bottom of the page, so all the data is collected.
* Add all event data via an inline script in the footer to ensure all the data is collected in time.
*
* @return void
*/
public function inline_script_data(): void {
$this->set_script_data(
'settings',
array(
'tracker_function_name' => self::tracker_function_name(),
'events' => $this->get_enabled_events(),
'identifier' => self::get( 'ga_product_identifier' ),
)
);

wp_register_script(
$this->data_script_handle,
'',
Expand All @@ -112,7 +140,7 @@ public function inline_script_data(): void {
wp_add_inline_script(
$this->data_script_handle,
sprintf(
'wcgai.trackClassicPages( %s );',
'var ga4wData = %s;',
martynmjones marked this conversation as resolved.
Show resolved Hide resolved
$this->get_script_data()
)
);
Expand Down Expand Up @@ -214,29 +242,22 @@ public static function tracker_function_name(): string {
*
* @return array
*/
public function get_analytics_config(): array {
$defaults = array(
'gtag_id' => self::get( 'ga_id' ),
'tracker_function_name' => self::tracker_function_name(),
'track_404' => 'yes' === self::get( 'ga_404_tracking_enabled' ),
'allow_google_signals' => 'yes' === self::get( 'ga_support_display_advertising' ),
'logged_in' => is_user_logged_in(),
'linker' => array(
'domains' => ! empty( self::get( 'ga_linker_cross_domains' ) ) ? array_map( 'esc_js', explode( ',', self::get( 'ga_linker_cross_domains' ) ) ) : array(),
'allow_incoming' => 'yes' === self::get( 'ga_linker_allow_incoming_enabled' ),
),
'custom_map' => array(
'dimension1' => 'logged_in',
public function get_site_tag_config(): array {
return apply_filters(
'woocommerce_ga_gtag_config',
array(
'track_404' => 'yes' === self::get( 'ga_404_tracking_enabled' ),
'allow_google_signals' => 'yes' === self::get( 'ga_support_display_advertising' ),
'logged_in' => is_user_logged_in(),
'linker' => array(
'domains' => ! empty( self::get( 'ga_linker_cross_domains' ) ) ? array_map( 'esc_js', explode( ',', self::get( 'ga_linker_cross_domains' ) ) ) : array(),
'allow_incoming' => 'yes' === self::get( 'ga_linker_allow_incoming_enabled' ),
),
'custom_map' => array(
'dimension1' => 'logged_in',
),
),
'events' => self::get_enabled_events(),
'identifier' => self::get( 'ga_product_identifier' ),
tomalec marked this conversation as resolved.
Show resolved Hide resolved
'consent_modes' => self::get_consent_modes(),
);

$config = apply_filters( 'woocommerce_ga_gtag_config', $defaults );
$config['developer_id'] = self::DEVELOPER_ID;

return $config;
}

/**
Expand Down
Loading