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

Conversation

martynmjones
Copy link
Contributor

@martynmjones martynmjones commented Mar 13, 2024

Changes proposed in this Pull Request:

The changes to how the tracker is initiated in version 2.0.0 has caused problems with more sites than anticipated. Primarily those problems come from plugins that minify and combine JS files as many of them disregard listed dependencies while doing so.

As it currently stands there are many optimisation plugins that do work well with Google Analytics for WooCommerce but to make the extension more resilient against unexpected scenarios from different optimisation techniques this PR restructures how things execute.

  • dataLayer and gtag (or a function using the filtered tracker var name) are no longer included in the JS file. Instead they are added directly to the page <head>
    • This is closer to how it was functioning pre-2.0.0
    • Does not use the WordPress script queue system to prevent this block of code being moved to anywhere else
    • Hooks into wp_head early so that the script is added as soon as possible. This makes it accessible to other plugins and means we are able to rely on it being available on the page regardless of when our scripts load 582e41b
  • Relies on the DOMContentLoaded event to ensure inline script data is available at the point we begin tracking events
    • E2E tests have been added to cover some of the scenarios we've seen with optimisation plugins that has caused problems with this 8e7481d
  • Restructures the tracker to be more functional 9fede11

Detailed test instructions:

  1. Build extension from update/separate-site-tag-setup-and-event-tracking
  2. Run smoke tests to confirm all events are tracked
  3. Install an optimisation plugin like Autoptimize
    • Configure the extension to combine and minify all Javascript
  4. Check the page source to confirm that gtag is still setup in the page <head>
  5. Re-run smoke tests to confirm events are still tracked

Additional details:

  • An adjustment was made to the selector for the related products add to cart E2E test as it was occasionally failing if a variable product was the first displayed in the related products list 25bd09c

Changelog entry

Update - Separate the site tag from the event tracking file and delay execution until DOMContentLoaded

@github-actions github-actions bot added the changelog: update Big changes to something that wasn't broken. label Mar 13, 2024
@tomalec tomalec force-pushed the update/separate-site-tag-setup-and-event-tracking branch from 0ac886d to 5a9d161 Compare March 13, 2024 18:39
@tomalec
Copy link
Member

tomalec commented Mar 13, 2024

I have quite a revolutionary proposal, to change the Tracker from singleton to just a function and remove the config file: https://github.com/woocommerce/woocommerce-google-analytics-integration/compare/update/separate-site-tag-setup-and-event-tracking...update/separate-site-tag-setup-and-event-tracking-new-tracker?expand=1#diff-a745b91bc9c87614ca4293b765bb7a9053681512cb2cce7d9158b6e2946048b8R95

quite revolutionary, but TBH, at the stage of 2.0.0, I think separating tracker and postponing blocks & classic tracking after dom content loaded could be disrupting anyway.

WDYT?

Comment on lines 63 to 64
// 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

@tomalec tomalec force-pushed the update/separate-site-tag-setup-and-event-tracking branch from 7b3ef32 to c241828 Compare March 13, 2024 19:46
@tomalec
Copy link
Member

tomalec commented Mar 13, 2024

If we aim to support "optimization" plugins, it seems there is another plugin that turns parges into static ones, which undermines the entire idea of setting any data in PHP. See #399

remove config file, pass settings and configs as arguments.
@martynmjones martynmjones marked this pull request as ready for review March 22, 2024 14:39
@martynmjones martynmjones requested a review from a team March 22, 2024 14:39
@tomalec
Copy link
Member

tomalec commented Mar 23, 2024

Does not use the WordPress script queue system to prevent this block of code from being moved to anywhere else

I wonder if we really need to go that extreme. Pre 2.0.0 implementation used wp_add_inline_script and I assume it was working ok, with other extensions. Or maybe you have found an extension that breaks when we put dependency-less inline script using WP queue system?

I'm concerned that if we were using the WP queue before, then maybe there are extensions in the wild that rely on that. For example, there may be a need to use the WP queue system to hook their scripts before/after to add/overwrite some gtag config with precise timing.

Copy link
Member

@tomalec tomalec left a comment

Choose a reason for hiding this comment

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

Tested locally with and w/o Autooptimizer, and reviewed the code. LGTM
I'd appreciate someone reviewing my code :)

Copy link
Contributor

@mikkamp mikkamp left a comment

Choose a reason for hiding this comment

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

I left some additional comments as I looked through the code.

As @tomalec mentioned, I'm also a bit concerned about the use of wp_print_inline_script_tag compared to the previous use of wp_add_inline_script.

There is no filter for the contents of wp_print_inline_script_tag only for the script attributes. Whereas wp_add_inline_script allows us a lot more flexibility. That becomes evident with the GLA compatibility where it isn't able to set config data right after the snippet in the head.

assets/js/src/index.js Outdated Show resolved Hide resolved
assets/js/src/index.js Outdated Show resolved Hide resolved
Comment on lines 75 to 79
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() )
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we tied to legacy function names / visibility here? Just looks confusing to be mixing self and $this.

Copy link
Member

Choose a reason for hiding this comment

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

I had a bigger concern with the self:: functions, especially self::get.
But I didn't want to push it here, not to refactor everything at once.
We use a lot of static functions that are not 100% "static," IMO.

PHP spec

Declaring class properties or methods as static makes them accessible without needing an instantiation of the class.

So we could change self::get( * ) to $this->get() or event $this->settings[ * ] (IMHO, get just blurs the picture here w/o much benefit).

Do you also suggest changing tracker_function_name, DEVELOPER_ID, and get_consent_modes to be instance methods/props?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest in this case to access the methods through the current instance, which will make it easier in the future to remove static from those function declarations. We could do that now but then code snippets / other plugins would have a little harder time getting the instance.

Suggested change
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() )
esc_js( $this->get( 'ga_id' ) ),
esc_js( $this->tracker_function_name() ),
esc_js( self::DEVELOPER_ID ),
json_encode( $this->get_consent_modes() ),
json_encode( $this->get_site_tag_config() )

Generally the purpose of the get helper function would be to enforce both the parameter type for key and the return value. However it seems the return value for non_scalar values is broken. If you do something like this you'll get a fatal error:

self::$settings['ga_id'] = [];
var_dump( $this->get( 'ga_id' ) );

Where we do see it working is for example if we do something like:

self::$settings['ga_id'] = 1;
var_dump( $this->get( 'ga_id' ), self::$settings['ga_id'] );

The first value will be a string, but the second will be an int, so we will be less certain what kind of value we will be working with if we don't use the get method.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks :) I changed the reference in non-static methods to use $this-> in 867f696 (plus tweaked self::DEVELOPER_ID to static::DEVELOPER_ID, to suppress the notice).

Let's change the static in methods in a separate PR and release. I'll start a draft to keep this idea on track

Copy link
Member

Choose a reason for hiding this comment

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

Generally the purpose of the get helper function would be to enforce both the parameter type for key and the return value. However it seems the return value for non_scalar values is broken. If you do something like this, you'll get a fatal error:

I wonder if we really need such behavior. I was experimenting with adding array-like settings for consent modes, and this stringification behavior was more of a problem than a help. But that's also something to address in a separate PR

Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely limiting, and assuming of what settings are like. Agree that it would be better handled in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

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

Added an issue to track that enhancement idea #412

includes/class-wc-google-gtag-js.php Outdated Show resolved Hide resolved
* @return void
*/
public function setup_site_tag() {
wp_print_inline_script_tag(
Copy link
Contributor

@mikkamp mikkamp Mar 26, 2024

Choose a reason for hiding this comment

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

Does printing the snippet directly have any consequences for other plugins extending the code?

These solutions are not as compatible anymore: https://github.com/woocommerce/google-listings-and-ads/blob/2.6.4/src/Google/GlobalSiteTag.php#L220-L237

In theory the filter woocommerce-google-analytics-integration still works, but that's added much later and not right after the snippet in the head (the timing when we want to set configuration).

We can't use the filter woocommerce_ga_gtag_config because we don't want to add additional config parameters, we want to add tracking to a second GA_ID. Maybe moving the three gtag lines to an array of strings and then filtering the array before each line is added would be more flexible.

@tomalec
Copy link
Member

tomalec commented Mar 26, 2024

Thank you, @mikkamp, for the detailed review!
I addressed some of your comments. AFAIK, @martynmjones is testing whether there are some extreme optimization plugins that would cause problems with a regularly enqueued inline script with gtag definition.
In the meantime, I'd appreciate your answers in the remaining threads.

Copy link
Contributor

@mikkamp mikkamp left a comment

Choose a reason for hiding this comment

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

Thanks for addressing the additional comments, I've added some additional thoughts there.

At this point the only thing in this PR that I'm a little on the fence about is:

There is no filter for the contents of wp_print_inline_script_tag only for the script attributes. Whereas wp_add_inline_script allows us a lot more flexibility. That becomes evident with the GLA compatibility where it isn't able to set config data right after the snippet in the head.

GLA still injects triggers for the events directly within the page body, so we'll want to ensure that the configuration is set early (in the head). One of the following options could resolve that:

  1. Allow filtering of the snippet added in wp_print_inline_script_tag (the whole snippet, or filter for adding extra lines)
  2. Revert back to using wp_add_inline_script so we can enqueue configuration right after the script.

In addition to that I think if we'd pick like 3 or 4 of the most popular optimizer plugins, if we are compatible with more or less the default configurations for optimizing JS then I'd see that as acceptable to release this PR.

@martynmjones
Copy link
Contributor Author

martynmjones commented Mar 28, 2024

In addition to that I think if we'd pick like 3 or 4 of the most popular optimizer plugins, if we are compatible with more or less the default configurations for optimizing JS then I'd see that as acceptable to release this PR.

I've run some tests with a few of the popular optimisation plugins using the different configurations on offer. There have been two issues with Autoptimize, Aggregate inline JS as you pointed out earlier, and forcing the JS script to load in the <head>.

If main.js is moved to the <head> then it will continue working and we have a test to cover that scenario. The problem with that setting in Autoptimize is that dependencies are ignored so several things break and not just Google Analytics for WooCommerce. So I don't think we need to make adjustments to try to have the extension work with that setting.

For Aggregate inline JS, the easiest fix is to add ga4w to the list of excluded scripts/ variables in Autoptimize. So an option would be to keep things as they are an add a "Known incompatibilities" section to the docs and explain to merchants who want to have inline JS aggregated how to exclude the script?

Plugin Name Minify JS Combine JS Files Aggregate inline JS Defer JS Delay Execution Force JS in <head>
Autoptimize n/a
WP-Optimize n/a n/a n/a
WP Rocket n/a n/a
W3 Total Cache n/a
LiteSpeed Cache n/a

@tomalec
Copy link
Member

tomalec commented Mar 28, 2024

Thanks for the nice comparison @martynmjones !

I think we can try to make workarounds to make our code work if the scripts are aggregated, loaded in random order, or extracted to external JS. They question is: do we want to?

Make JS work even if data is loaded from an external defered async script or an external file—but if someone puts that data into an external file, is it still dynamically populated with the data we collect with hooks?

We could assume it is. But if it's not it may silently corrupt the behavior, which will be hard to identify by HEs.


Alternatively, we could make JS fully static, defer, and async by default, and put the data into HTML, like:
<wc-ga4w event-data="{JSON with data}">
This will let all the optimizers optimize the JS, which could be cached, deferred, etc, but we'll keep our data safe in the markup.
And also would me it easy to pick up the timing when the data gets there: customElements.define('wc-ga4w', class extends HTMLElement{ attributeChangedCallback });

Then:

  • if JS is loaded before the element is stamped to the document, the element will do the job when it's ready
  • otherwise, the element will do the job once it's upgraded == when the JS gets loaded
  • our JS could have the load event listener to throw a warning if there is not document.querySelector('wc-ga4w') after the document is complete` => meaning the JS was loaded but expected data is missing.

Plus, we'll have the good old separation of concerns: JS is for behavior, HTML is for the content.

warn if still not available after the document is fully loaded.

Make the extension work, regardless of the order and timing of  `woocommerce-google-analytics-integration` and `woocommerce-google-analytics-integration-data`scripts.

Addresses #398 (comment)

Make it work with Autoptimize "Aggregate inline JS" option, address #398 (comment)
@tomalec
Copy link
Member

tomalec commented Mar 28, 2024

@martynmjones @mikkamp I made a change in our logic flow and conditions: dacaabc
Now:

  • woocommerce-google-analytics-integration-data sets the data global and dispatches an event to document
  • woocommerce-google-analytics-integration:
    • Use the data if it's already there, to process it ASAP
    • If it's not there:
      • Add an event listener for our own event to initialize once data is ready,
      • Warn if/once the document is fully loaded, and the data is still missing.

I believe this way, we're more bulletproof towards other extensions messing up with our order and timing.

Pros:

  • Autoptimize "Aggregate inline JS" works
  • I'm not sure how about "Force JS in <head>" as for me, it breaks other parts of Woo, but theoretically, it should work as well, as long as both scripts eventually get somewhat loaded.
  • More lightweight than a custom element implementation.

Cons:

  • Autoptimize "Aggregate inline JS" works; however, it puts dynamic, cart-specific data into a cacheable, external JS file, which would malform the data silently.
  • "Pollutes" the document with one event
  • Will keep showing warnings for folks who load woocommerce-google-analytics-integration in blocking fashion but woocommerce-google-analytics-integration-data - but maybe that's actually good thing
  • It assumes ga4w:ready means window.ga4w is ours and ready. Will not protect us against somebody triggering ga4w:ready event on their own w/o window.ga4w available. Custom element way would have a bit more integrity in that regard. But I guess we cannot protect against everything.
  • Puts a bit more JS in PHP string

@mikkamp
Copy link
Contributor

mikkamp commented Mar 29, 2024

Thanks @martynmjones for all the testing, that's a great result, and I think sufficient enough to move forward.

@tomalec I like your last addition and I think that makes it a bit more robust.

it puts dynamic, cart-specific data into a cacheable, external JS file, which would malform the data silently

Having the cart specific data here is not the most ideal solution. Instead we should be fetching it from the cart fragments or the data used to populate the mini cart, but that's getting a little out of scope and goes beyond what we had pre 2.0

As for the other cons you listed, I think it's good to be aware of them, but not necessarily something we want to protect against. Placing some expectations in the documentation would probably be a better way to go about it for now.

So from my perspective this PR is pretty much ready to go. I'm just not sure what the final conclusion to this comment was: #398 (comment)
Are we still looking to make that more flexible here?

@martynmjones martynmjones merged commit 7137073 into trunk Apr 4, 2024
6 checks passed
@martynmjones martynmjones deleted the update/separate-site-tag-setup-and-event-tracking branch April 4, 2024 17:43
@martynmjones
Copy link
Contributor Author

So from my perspective this PR is pretty much ready to go. I'm just not sure what the final conclusion to this comment was: #398 (comment)
Are we still looking to make that more flexible here?

Thanks for all the input @mikkamp 👍 I updated the PR so it's closer to how we were previously doing it 77067d8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog: update Big changes to something that wasn't broken.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants