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

Consent Mode not updated with page caching #399

Closed
toktor opened this issue Mar 13, 2024 · 10 comments
Closed

Consent Mode not updated with page caching #399

toktor opened this issue Mar 13, 2024 · 10 comments
Labels
needs investigation The issue/PR needs further investigation. v2.0.0 Related to version >= 2.0.0

Comments

@toktor
Copy link

toktor commented Mar 13, 2024

Describe the bug:

When using WP-Rocket and caching enabled, we can not change the consent state via woocommerce_ga_gtag_consent_modes filter.
The issue lies as the consent mode is included as inline JS and WP-Rocket creates static files for each page where we can not change the inline JS.

Steps to reproduce:

  1. Have site with WP-Rocket page-cache enabled
  2. Send data to woocommerce_ga_gtag_consent_modes when user is logged out and modifying consent
  3. WP-Rocket serves previous version of JS with previous consent

Expected behavior:

  1. Possibility to change consent on the fly without clearing page cache
  2. Not serve wrong consent for customers who get served static page cache

Actual behavior:

  1. Not possible to change consent on the fly without clearing cache. This can also create issue of cross-user-cache where one consent state might end up in the wrong user view as the cache file is the same. At least thats how I understand.

Additional details:

Possible proposed solution:

  1. I think the issue lies with inline JS in here:

Simplest solution would be to change this JS (or at least consent part) to be included JS file. In this case we can exclude it from caching separately.

  1. Alternative method would be to include Consent States with JS only and not have them come from PHP. Ex. Ajax call to wp-admin to add consent settings.

  2. Third option would be to find a filter in JS that runs the event code and attach to this. So while we do print "wrong" consent modes in the html, we can listen with JS once the code starts running and modify the consent states on the fly when they are sent - without touching the html.

Background:
We have created a bridge to use Complianz plugin to manage consent itself and we are using WGAI to show consent. That means we have a bridge that sends consent from Complianz to WGAI but as of not, we cant update consent properly. We cant use Complianz either to manage consent in tags as it does not work together with WGAI. But I assume this issue would be with any of the caching and consent plugins/solutions where we need to update the consent dynamically.

Please let me know if I have understood the problem correctly. I can propose a solution/code modification, if the possible solution is something that makes sense.

Please let me know if this bug-report does not belong here or should be somewhere else.

EDIT:
I missed the part where it is recommended to update on-page consent with gtag. We can do this but the default page state update would still be nice.

@tomalec
Copy link
Member

tomalec commented Mar 13, 2024

Hi @toktor, thanks for early adopting the latest changes and for the detailed report :)

Supporting WP Rocket may be tricky. I haven't investigated it deeply yet, but from a high level, there may be more problems than just consent mode if the plugin turns every page into a static one.

The way the extension works is that it adds inline JS with dynamic data for each page, which may contain information about products added to the cart, purchases, etc. Before 2.0.0 (before consent mode support), it was even more dynamic, as the extension was inlining gtag calls depending on the state. So, having the pages cached could result in irrelevant things being tracked.

As per the consent mode itself:
The idea behind the default state = the thing our extension set = gtag('consent', 'default', {...} ), is that it represents your site configuration, and the consent set on the page loaded, before the user takes any action and before any event is tracked (even the page_view).
Then the consent could be updated in the page run-time, using gtag('consent','update',{...}) according to a user changing their consent via banner, loading their preferences, etc. (we do not implement any gtag('consnt','update'... in WCGAI).

So, speaking of potential solutions:

  1. Do I understand correctly that you suggest removing inline JS and replacing it with a blocking <script src="file.js"> where file.js would be dynamically created on the PHP side to contain consent modes?
  2. AJAX call for default setting could be trouble some due to the logic I explained above. To respect the Google API rules and users' rights, we'd have to hold all the actions and all the tags from our and other extensions until the AJAX response comes. I cannot imagine the way to do so. (but I'm always happy to be proved wrong :) )
  3. Adding a filter in JS seems more doable than above. But if you have access to JS, you can set your own gtag('consent', 'default',{}) after ours, overwriting the settings. Which is the way correct way to use Google's API.
    So, there is no need for the hook. You can synchronously add your own blocking script that does gtag('consent', 'default',{ the right one }), and it's done.

AFAIK The fallowing is the right way to use the Google's API and should work fine

<script>
   // WCGAI code with "wrong" defaults
   gtag('consent', 'default', {
	  'analytics_storage': 'denied',
	  'ad_storage': 'denied',
	  'ad_user_data': 'denied',
	  'ad_personalization': 'denied',
	  'region': ['PL', 'ES' /*...*/]
	});
</script>
<!-- later in the document, in your blocking script -->
<script>
   // your code, with the "right" defaults
   gtag('consent', 'default', {
	  'analytics_storage': 'allow',
	  'ad_storage': 'allow',
	  'ad_user_data': 'denied',
	  'ad_personalization': 'denied',
	  'region': ['PL', 'ES' /*...*/]
	});
	// ... asynchronously, after AJAX, the user click, whatever
	gtag( 'consent', 'update', {
	  'analytics_storage': 'allow',
	  'ad_storage': 'allow',
	  'ad_user_data': 'allow',
	  'ad_personalization': 'allow',
	  });
</script>

Thanks for giving the context and mentioning Complianz. I still have it on my to-do list to investigate the integration with them more deeply, but we planned that for v1 or v2 of consent mode. We wanted to ship MVP and basic manual/hook-based configuration ASAP.

Could you elaborate a bit more on how you do that bridge and what and why does not work together?

@tomalec tomalec added needs feedback The issue/PR needs a response from any of the parties involved in the issue. needs investigation The issue/PR needs further investigation. labels Mar 13, 2024
@toktor
Copy link
Author

toktor commented Mar 14, 2024

@tomalec thank you for the detailed response! I think I might have gone a bit off-topic but hope it might help with brainstorming.

Adding a filter in JS seems more doable than above. But if you have access to JS, you can set your own gtag('consent', 'default',{}) after ours, overwriting the settings. Which is the way correct way to use Google's API.
So, there is no need for the hook. You can synchronously add your own blocking script that does gtag('consent', 'default',{ the right one }), and it's done.

In current configuration WCGAI page_view is running straight after var wcgai is established with no room to change consent states. Tho I am not sure is consent important for that event.

So, having the pages cached could result in irrelevant things being tracked.

Yep, but I haven't seen ecommerce site that can afford to not cache pages. Cart is updated dynamically via ajax. Even with page cache, each page still has its product parameters correct. With consent now we have first time information that is per-user-basis.

Do I understand correctly that you suggest removing inline JS and replacing it with a blocking <script src="file.js"> where file.js would be dynamically created on the PHP side to contain consent modes?

Possibly, but that might delay loading this js so that your trackClassicPages already runs and therefore breaks. This would also go against WCGAI plan to run tracking as soon as possible for the rest of the world........

AJAX call for default setting could be trouble some due to the logic I explained above. To respect the Google API rules and users' rights, we'd have to hold all the actions and all the tags from our and other extensions until the AJAX response comes. I cannot imagine the way to do so. (but I'm always happy to be proved wrong :) )

I agree on that. I understand that your purpose is to run the events as early as possible and that would add considerable delay.

_

Thanks for giving the context and mentioning Complianz. I still have it on my to-do list to investigate the integration with them more deeply, but we planned that for v1 or v2 of consent mode. We wanted to ship MVP and basic manual/hook-based configuration ASAP.
_

Understood and this was a pleasant surprise finding it, thank you.

Could you elaborate a bit more on how you do that bridge and what and why does not work together?

I created a script that reads the consent state from cookie and passes it on to ajax and save it to session for now. From there I used session to set consent based on session data for the user. It was supposed to be prototype solution, but failed as soon as caching was enabled. Should have seen this one....

Current solution

What I have understood is, with correct consent banner, the acceptance rate could be as high as 80%. So we wanted to find a solution quickly for us. In my specific issue, as a workaround to get the sites back to tracking I came up with this solution.

Complianz updates consent only on their own custom trigger - "cmplz_fire_categories".

This will fire way after all the JS has already run and WCGAI has triggered its events. As a workaround, I wrapped this function in DOMContentLoaded event -

I also broke WCGAI gtag js function so it would not fire and allow Complianz to run its gtag later in the code (no way to modify priority either and its has its own issues with consent settings).
Right now Compilianz fires its config tag normally and trackClassicPages only runs after everything else is loaded and set.

Further digging reveals that cookiebot does manage to change consent before WCGAI triggers its events. It just runs so much earlier but their pricing....

I understand that WCGAI code is written so that it should fire as early as possible. In my case, I would be happy with a filter that would allow delaying WCGAI code (even php filter, to attach a custom parameter etc).

Alternatively, if page_view does not need consent, then I will write the solution you recommended and go from there.

I would be glad to help further, if you need anything. If you wish, I can set up a quick staging environment with Complianz for example.

@tomalec tomalec removed the needs feedback The issue/PR needs a response from any of the parties involved in the issue. label Mar 14, 2024
@tomalec
Copy link
Member

tomalec commented Mar 14, 2024

I find it perfectly on topic. :)

In current configuration WCGAI page_view is running straight after var wcgai is established with no room to change consent states. Tho I am not sure is consent important for that event.

Ah, you're right; that's where the JS hook would help. I'd say consent is important for every gtag event.

Yep, but I haven't seen ecommerce site that can afford to not cache pages.

That is a fair point, but AFAIK that's how WCGAI was working for all the versions <=1.x. With 2.0.0, we made a significant refactor moving lots of stuff to JS, to gradually improve, but we're not fully there yet, plus we have to support legacy use-cases and WooCommerce flows, which may stay non-cacheable forever.
I'm all for becoming 100% catchable, but I'm just saying, that's not consent mode that broke it, we've been broken before and need more steps to improve ;)

Cart is updated dynamically via ajax. Even with page cache, each page still has its product parameters correct.

But there are cases when there is no AJAX, especially for classic shortcode-based pages, w/o Gutenberg Block-based UI, and even some Block-based pages work inconsistently. (maybe @martynmjones would have 2c to add here)
For example purchase - "thank you" page, checkout, etc.

With consent now we have first time information that is per-user-basis.

(as mentioned above, we already have some per-user data with purchase, order, or cart in some cases, but..)

gtag( 'consent', 'defalt', {... region:...}) is not per-user setting. Is per site & region setting. For per-user, you can use gtag('consent', 'update'..., you could also use wait_for_update flag in your default state, to hold tags before an update you expect to come asynchronously:

// WCGAI config tweaked with PHP hook just to add `wait_for_update`
  gtag('consent', 'default', {
    'ad_storage': 'denied',
    // ...
    'wait_for_update': 500
    });
// ... sync code
// ... async code

// Complianz, or your code fetching per-user consent settings:
gtag('consent', 'update', {ad_storage: 'allow'})

Possibly, but that might delay loading this js so that your trackClassicPages already runs and therefore breaks. This would also go against WCGAI plan to run tracking as soon as possible for the rest of the world........

If you make it a blocking script it should not break. Speaking of plans, tracking ASAP, and calling page_view after config:

We're currently exploring changing a bit the approach, timing and lifecycle of scripts in

This way, we'd load inlined gtag definition with its config and default consent modes, and only that. That should be per-side fully catchable config. We'd load it synchronously before any event, so you could inject your stuff before or after and expand/overwrite gtag or consent config.

Then, we'd load all the classic and block integrations in the footer, maybe even after DOMContentLoaded, which you suggested as well. This way, you'd have plenty of time to tweak before we tag anything.

@tomalec
Copy link
Member

tomalec commented Mar 14, 2024

Complianz

This will fire way after all the JS has already run and WCGAI has triggered its events. As a workaround, I wrapped this function in DOMContentLoaded event -

It seems what we bake in #398 should align with your solution, correct?

I also broke WCGAI gtag js function so it would not fire and allow Complianz to run its gtag later in the code (no way to modify priority either and its has its own issues with consent settings).

Do you actually do that? It should be fine for Complianz gtag function be defined after ours, it should simply overwrite

I understand that WCGAI code is written so that it should fire as early as possible. In my case, I would be happy with a filter that would allow delaying WCGAI code

Do you need to delay all the code? or just firing events? Would it work for you, if we have the following code loaded synchronously

window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Set up default consent state.
for ( const mode of modes ) {
	gtag( "consent", "default", mode );
}
gtag("js", new Date());
gtag("set", "developer_id.###", true);
gtag("config", ...);

that AFAIK per docs could be later extended, then adding all the other code in a <script> deferred to the footer and executing stuff async on document loaded?

(feel free to add any feedback to https://github.com/woocommerce/woocommerce-google-analytics-integration/pull/398/files)


I would be glad to help further, if you need anything. If you wish, I can set up a quick staging environment with Complianz for example.

Thanks! :) A staging environment would be awesome. But even a quest, non-logged-in example of a site with Complianz would be helpful, to see how they inject consent mode and gtag definitions.

@tomalec tomalec added the v2.0.0 Related to version >= 2.0.0 label Mar 14, 2024
@toktor
Copy link
Author

toktor commented Mar 15, 2024

@tomalec wonderful reply, thank you.

It seems what we bake in #398 should align with your solution, correct?

It should yes. Here's where my knowledge of JS runs a bit thin, I need to see it running to understand. But yes, I think so.

Do you actually do that? It should be fine for Complianz gtag function be defined after ours, it should simply overwrite

Right now the "view_page" event is triggered by WCGAI with WCGAI default consents. I wanted to move it down a bit. By default config (the one that is in main.js in WCGAI) sends pageview event when it sets its config - https://developers.google.com/analytics/devguides/collection/ga4/reference/config#send_page_view

Though I could possibly set page_view to false before WCGAI I guess...

Again, in this case I am not anymore sure does pageview needs consents because at this configuration Complianz sends its pageview / config event also before it has set its consents.

Do you need to delay all the code? or just firing events? Would it work for you, if we have the following code loaded synchronously

I think just the events make sense. If events are delayed then we can go back and modify previous JS if really needed, other plugins can load their config and overwrite nessecary parts etc :)

I think after talking here and diving more into different plugins your filter was functioning as expected in the first place and the interaction with caching is feature, not a bug. As you said, its to establish a defaults not to update consent runningly. From my side, I will take back the "bug status" and if you wish, you can close the issue :)

Thanks! :) A staging environment would be awesome. But even a quest, non-logged-in example of a site with Complianz would be helpful, to see how they inject consent mode and gtag definitions.

Would you be willing to give me a place where I can send urls non-publicly?
I can send you DM in insta if you know to look for it or you can also send email to robert@robothead.eu and I will send you one cookiebot and one complianz page with WCGAI.

@martynmjones
Copy link
Contributor

martynmjones commented Mar 15, 2024

Sharing some thoughts after reading through the issue so far:

But there are cases when there is no AJAX, especially for classic shortcode-based pages, w/o Gutenberg Block-based UI, and even some Block-based pages work inconsistently. (maybe @martynmjones would have 2c to add here)
For example purchase - "thank you" page, checkout, etc.

With page caching the tracking for most events should be fine because like @toktor said, each page will also cache the correct product data. For the cart, checkout, and purchase pages they would all need the dynamic data but those pages would need to be excluded from the cache anyway so I don't imagine that should be an issue.

If custom mini-carts are used on product pages then we may see problems with the remove_from_cart event because it won't have the required product data available.

It seems what we bake in #398 should align with your solution, correct?

I tested Complianz with #398 along with aggressive page caching and it definitely seems to address the problems described with a few caveats.

Using a simple snippet (found below although it doesn't account for consent being revoked) to add an event listener for one of the custom Complianz events, I was able to use the extension and have it update the consent status via the gtag setup by Google Analytics for WooCommerce.

There were a few issues:

1. Events are sent before the consent state is updated on the page the user provides consent on

Complianz says that it blocks scripts from running until consent is given. It does block Google Tag Manager but does not block Google Analytics for WooCommerce.

The result of that is that as soon as the page loads, the dataLayer object is filled with our events.

When consent is then given, the consent update is pushed to dataLayer and after that Google Tag Manager loads so it's all out of order for what we want.

On subsequent page loads that is not a problem because we receive the approved consent state from Complianz and push that to dataLayer before event tracking is processed.

2. Complianz Consent state wasn't maintained across page loads

This may very well be because I'm not particularly familiar with Complianz and WP-Rocket but in testing I was often being prompted to grant consent over and over again although that doesn't look to be an issue with Google Analytics for WooCommerce.

Snippet used:

add_action( 'wp_head', function() {
	wp_print_inline_script_tag("
		document.addEventListener( 'cmplz_before_category', function( consentData ) {
			if ( consentData.detail.category === 'statistics' ) {
				gtag( 'consent', 'update', {
					'analytics_storage': 'granted'
				} );
			}
			if ( consentData.detail.category === 'marketing' ) {
				gtag( 'consent', 'update', {
					'ad_storage': 'granted',
					'ad_user_data': 'granted',
					'ad_personalization': 'granted'
				} );
			}
		});");
});

So while some events may encounter problems on sites with aggressive caching and custom cart implementations, the consent functionality and general tracking should continue working as soon as we've finished work on #398.

@martincpt
Copy link

I'm also facing with the same issue but with LiteSpeed Cache and CookieYes. I'm still using the older version of this plugin on my other heavily cached website without extraordinary issues so I don't think cache is a big issue here.

There is two main problems here.

  1. Because of the caching, wordpress filters cannot be used here anymore, as the latest cache and it's consent state would alter other users tracking settings, thus it would be inconsistent more probably.
  2. GA is automatically sending page view event.

For now, I came up with the following solution.

  1. I let load everything denied, missing out the default GA page_view event.
  2. I send an update of consent modes as advised, right after WGAI inline loads.
  3. I send the page_view event manually.

I haven't tested this solution much, but the page tracking immediately appeared on the real time events page.

This is the code I fire on every page load and on user indicated consent mode update.

function update_consent(){
	var store = window.cookieyes._ckyConsentStore;
	if (!store) return;

	var analytics_enabled = store.get("analytics") === 'yes';
	var analytics = analytics_enabled ? 'granted' : 'denied';
	var ads = store.get("advertisement") === 'yes' ? 'granted' : 'denied';
	var grants = {
		'ad_storage': ads,
		'ad_user_data': ads,
		'ad_personalization': ads,
		'analytics_storage': analytics,
	};
	
	var consent = window.dataLayer.find(i => i[0] == "consent");
	if (consent) {
		mode = JSON.parse(JSON.stringify(consent[2]));
		for (var key in grants) {
			mode[key] = grants[key];
		}

		gtag('consent', 'update', mode);

		if (analytics_enabled) {
			gtag('event', 'page_view', {
				page_title: document.title,
				page_location: window.location.href,
			});
		}
	}
}

Personally, I would love to see a solution where the WGAI inline JS hooks up for a possible callback function to set the final consent modes before pushing them to the dataLayers.

@tomalec
Copy link
Member

tomalec commented Apr 29, 2024

Thanks @martincpt for the feedback!
I still have it on my todo list to dig deeper into CookieYes integration.

In the mean time to address the simpler one:

GA is automatically sending page view event.

Our extension does not send this event explicitly. It's the Gtag config that's responsible to sent it automatically or not. You can tweak it using the woocommerce_ga_gtag_config filter, like:

add_filter( 'woocommerce_ga_gtag_config', function ( $config ) {
    $config['send_page_view'] = false;
   return $config;
} );

Is that enough of a solution for you? Or would you rather see it in the UI settings?

@martincpt
Copy link

Thanks, @tomalec! I didn't know about this setting. It looks like it fits my pattern, ensuring that page views are not being sent twice. I'll try it as soon as I get the chance.

@tomalec
Copy link
Member

tomalec commented Jun 18, 2024

Hi @martincpt & @toktor, in 2.1.x we released the improved integration with WP Consent API, which integrates with Complianz and other plugins.
Therefore, after installing those two plugins, you should have on-page, JS-based, consent updates working more-or-less out of the box.

I'm closing the issue. Feel free to reopen, or create a new one if you still encounter any problems.

@tomalec tomalec closed this as completed Jun 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs investigation The issue/PR needs further investigation. v2.0.0 Related to version >= 2.0.0
Projects
None yet
Development

No branches or pull requests

4 participants