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

Offer opt out without iframe / 3rd party cookies #17452

Closed
tsteur opened this issue Apr 12, 2021 · 14 comments · Fixed by #19528
Closed

Offer opt out without iframe / 3rd party cookies #17452

tsteur opened this issue Apr 12, 2021 · 14 comments · Fixed by #19528
Assignees
Labels
c: Privacy For issues that impact or improve the privacy. Critical Indicates the severity of an issue is very critical and the issue has a very high priority.
Milestone

Comments

@tsteur
Copy link
Member

tsteur commented Apr 12, 2021

3rd party cookies work less and less and eventually won't be available anymore. Kind of related post https://matomo.org/blog/2020/02/new-cookie-behaviour-in-browsers-may-cause-regressions/ For the opt in to work we therefore need a different way and set first party cookies.

Matomo for WordPress already doesn't use the opt out iframe anymore and sets first party cookies. In On-Premise as part of #12767 we already added the support of postMessages to set first party cookies when possible. This however currently only works in some cases (eg when the tracking code is embedded on the same page and both opt out and tracking code use the same Matomo domain).

In the future ideally we show a message in the opt out iframe when it won't work because eg there's no tracking code on the privacy policy page. We might even want to completely remove the third party cookie part (however we'd still need to detect it when it's set and not track to not break BC and to not suddenly start tracking users that oped out previously). Maybe we could even remove the domain check and opt users out in more cases even if there is a mismatch between the opt out iframe and the tracking domain on the privacy policy page.

Or maybe we would need to offer a new way of embedding the opt out without any iframe. This would likely require loading another JS and some configuration to customise it and for Matomo to know where to place it (unless this is all stored in a JS file and the user can configure multiple different JS opt out files).

@tsteur tsteur added the c: Privacy For issues that impact or improve the privacy. label Apr 12, 2021
@tsteur tsteur added this to the 4.5.0 milestone Apr 12, 2021
@Findus23
Copy link
Member

In theory something like this might work and be user-friendly, right?

<div id="opt-out"></div>
<script data-id="opt-out" src="https://matomo.example/optout.js"></script>

With optout.js being a file like matomo.js that is able to read the data parameter and then use it as a target of https://developer.matomo.org/guides/tracking-javascript-guide#optional-creating-a-custom-opt-out-form to create an opt-out box that looks like the iFrame.

Only issue is maybe localisation as this might bloat the js file quite a bit.

@tsteur
Copy link
Member Author

tsteur commented Oct 21, 2021

Generally yes @Findus23 It might not be a fixed JS file though but more of an actual request to take into account for example translations. Unless we can solve translations in JavaScript.

The current iframe opt out, also has few other options to customise colors etc:

<iframe
        style="border: 0; height: 200px; width: 600px;"
        src="/index.php?module=CoreAdminHome&action=optOut&language=en&backgroundColor=&fontColor=&fontSize=&fontFamily="
        ></iframe>

We would provide the same options for this. For example using data attributes like data-font-color="".

We also offer options to customise cookie domain, cookie same site, cookie secure, cookie path, cookie name prefix.

The rendered content should look and function otherwise the same as before. However, the cookie is set on the site that you're on and not on the Matomo instance.

@justinvelluppillai justinvelluppillai modified the milestones: 4.7.0, 4.8.0 Jan 18, 2022
@justinvelluppillai justinvelluppillai modified the milestones: 4.8.0, 4.9.0 Mar 1, 2022
@tsteur tsteur added the Critical Indicates the severity of an issue is very critical and the issue has a very high priority. label Mar 1, 2022
@gruniversal
Copy link

I made a very simple JavaScript opt-out to circumvent the iframe on some of my projects:
https://gitlab.com/gruniversal/erwin/-/blob/master/src/extensions/MatomoOptOut/MatomoOptOut.js

It also takes care of localisation by recognizing the browser-language (in a very hardcoded way).

I would be glad to see an "official" non-iframe solution so I can get rid of this some time.

@justinvelluppillai justinvelluppillai modified the milestones: 4.9.0, 4.10.0 Apr 12, 2022
@sgiehl sgiehl modified the milestones: 4.10.0, 4.11.0 May 5, 2022
@justinvelluppillai justinvelluppillai modified the milestones: 4.11.0, 4.12.0 Jun 7, 2022
@bx80
Copy link
Contributor

bx80 commented Jun 9, 2022

I'm proposing to implement this in the following way:

Change the Privacy Manager -> Users opt-out UI to generate HTML code using a <div> and <script> call instead of an <iframe>.

Existing iframe code will still be honored using the existing CoreAdminHome.optOut API method and optOut.js which will not be changed.

The generated code would look something like this:

<div id="matomo-opt-out"></div>
<script data-id="matomo-opt-out" data-background-color="#444488" data-language="en"
 src="https://matomo.example/plugins/CoreAdmin/javascripts/optOutDiv.js">
</script>

The optOutDiv.js fixed JavaScript will wait for the tracker code to finish loading, then read the script tag data attributes and use them to make an AJAX call to a new API method which will populate the opt out div with text and form elements translated into the chosen language, with font and color options.

Finally the script will listen for opt out / opt-in events and use _paq.push(['forgetUserOptOut']) / _paq.push(['optUserOut']); to update the tracker and set the first party cookie.

Supported appearance options would be: data-language, data-background-color, data-font-color, data-font-size and data-font-family.

The options to customize cookies would perhaps be better set on the tracker code?

The data-language attribute could support an option to use the detected browser language instead of a specific language value with an option like data-language="auto", similar to @gruniversal's solution.

The opt-out div could of course be positioned anywhere on the page and have additional styling applied as required.

@tsteur, @sgiehl Are you happy with this approach? Thoughts welcome 🙂

@bx80 bx80 self-assigned this Jun 9, 2022
@tsteur
Copy link
Member Author

tsteur commented Jun 9, 2022

@bx80

thanks for this. That's already going in the right direction. I've had a look around how other systems handle it and it's somewhat similar concepts. Sometimes they also directly give you the HTML to embed this like

<input type="checkbox" id="foo">You are currently opted out. Check this box to opt-in.
<script>
setupOptOut('foo');
function setupOptOut(id){
	// toggle checkbox
	// set cookie or paq.push
}</script>

Benefits: Lot of flexibility for users to change things and use their language/words etc, no extra requests that may be blocked by tracking blockers
Downside: Looks more complicated, should there be any bug or change needed then it's a problem as they would need to adjust the logic.

Maybe we'd just suggest this as an alternative method for people who want to completely customise it and it's in fact already documented here: https://developer.matomo.org/guides/tracking-javascript-guide#optional-creating-a-custom-opt-out-form . It be great to mention this on the page in the Admin UI "Let users opt-out of tracking" and link to it.

Existing iframe code will still be honored using the existing CoreAdminHome.optOut API method and optOut.js which will not be changed.

Great 👍

So the approach sounds generally good. Below some thoughts to think about

  • it be great for users to be able to optionally block the introduction text so that only the label and checkbox itself is shown (and not the text starting with "You may choose to prevent this ...")
  • is the initial extra request needed that then makes a request to the API? Or we could just return a dynamic JavaScript from a controller action? Then similarly as before we could support the same URL parameters as before to customise the look. The only difference being that we would now return a JavaScript instead of HTML in an iframe. This would maybe also make it easier for users to migrate from the current iframe solution as they could mostly reuse the same URL parameters if they did customise things. Not sure it's clear what I mean? Eg previously it would have been ...&action=optOut&language=en and now it could be ...&action=optOutJs&language=en. That might make it more straight forward to migrate and removes a request and it may even be easier to re-use validation of these paraeters etc so we don't need to have them in JS and in PHP in the iframe logic etc? It might also make it easier to migrate the optOut customiser in the UI.
  • Are there any thoughts around how we get users to switch from the no longer working iframe to this new solution? Maybe a "What's new" entry? And a short blog post mentioning this new optOut is available now and recommended and few steps on how to migrate to this and why they should migrate?

Finally the script will listen for opt out / opt-in events and use _paq.push(['forgetUserOptOut']) / _paq.push(['optUserOut']); to update the tracker and set the first party cookie.

Note that this may not work if people:

  • Don't have the tracking code embedded on the same site
  • or an ad blocker blocked the loading of the JS tracker
  • or if they don't even create a tracker instance because the website visitor maybe didn't consent to be tracked in their consent screen
  • or they use Piwik.getTracker() instead of creating a so called "async tracker"

The question be how could we detect if this is the case? And what do we do in such a case? We might need to show an error after a while that opt out is not possible if one of these things are happening and that they need to implement a custom opt out for example. The tracker may be loaded with a delay so we might not be able to detect it right away and only after a while.

Also we need to find out if the user is currently opted out or not to show the correct state (and an async tracker needs to be set up). Maybe it would mean we only show the content once we retrieved the current opt out/in status from the tracker?

If we generate the JavaScript for OptOut in a controller action (instead of an API), then depending if a user is logged into their Matomo account we could potentially show error messages for these people while for not logged in users we maybe keep the content empty so they don't see these errors. Just a thought. It may not be clear though because developers who integrate this may not be logged into Matomo and wouldn't know what the problem is. Then we'd maybe at least want to show this in the console or also in the UI etc.

The options to customize cookies would perhaps be better set on the tracker code?

Ideally yes. However, it does require that the tracking code is loaded and that an async tracker has been created. I wonder if we need a fallback for when this is not the case or if we just show error messages etc. Of course if this optOut script was to set cookies, then they would need to have the ability to specify cookie path etc and it would need to match the tracker settings which can be tricky to maintain. Otherwise we might set cookies on the wrong domain. So there's clearly a benefit letting the tracker set the cookies.

So generally it looks already quite good and just wanted to bring up a few things to further think about.

I guess the biggest question is how do we make sure the optOut will actually work for users in case eg _paq.push wouldn't work there (or the tracker instance was not created yet ...).

@bx80
Copy link
Contributor

bx80 commented Jun 10, 2022

Thanks @tsteur

I had thought about returning dynamic JavaScript, if that's something we're okay doing then it's definitely a cleaner solution with fewer calls 👍

Implementation plan changes:

✔️ Instead of serving fixed JavaScript and using data attributes, we add a new controller action optOutJs
which takes the exact same URL parameters as the iFrame action but returns dynamic JavaScript with the text translations done inline.

<div id="m-opt-out"></div>
<script src="/index.php?module=CoreAdminHome&action=optOut&language=en&backgroundColor=FFF&div=m-opt-out">
</script>

✔️ On the Privacy Manager -> Users opt-out UI we can add an alternative option for more customization where we provide a self-contained version of the opt-out code that doesn't make any API calls but will need to be customized, eg. it won't be translated. Additionally we show a link the custom opt-out form guide.

✔️ Provide a new opt-out code generation option to skip the introduction text.

✔️ Write a blog post explaining the advantages of the new opt-out approach and showing how to easily migrate existing sites, add a What's new? entry which links to the blog post.

Missing JS tracker fallback:

If we want to support opt-out when something has prevented the JS tracker from loading then I think a fallback option where we set the cookie directly could work. So if the JS tracker is loaded elsewhere then it can find the cookie and knows not to track. The current optOut.js code tries to find the JS tracker for three minutes before giving up, this might be too long?

The optOutJs logic could look something like this:

opt-out-logic

Does that sound ok?

@tsteur
Copy link
Member Author

tsteur commented Jun 10, 2022

Sounds overall good @bx80

  • Maybe we only wait like 30 seconds or so instead of 2 minutes for an error message?
  • We also need to double check how to honour existing opt outs. They would be mostly set as a third party cookie so not sure if we can honour and read them if people opted out previously? I guess we can't read them but not sure. FYI you might have already seen the existing logic. Whenever we can already set first party cookies we were using these methods to opt out & opt in & detect current status: https://github.com/matomo-org/matomo/blob/4.11.0-rc1/js/piwik.js#L7286-L7300 we would likely need to reuse these to keep showing the correct status.
    • I just realise because we make a request to the controller to generate the JS, the system can also likely still read the third party cookie if supported by the browser meaning we might still be able to honour this. However, removing this cookie wouldn't be possible (hence why we have this issue), so I guess if we were to honour this 3rd party ignore cookie a visitor would never be able to opt-in again (but that's maybe unlikely anyway). To be decided maybe what to do here best?
  • Writing direct cookie using default params I guess would work in most cases (unless subdomains etc) 👍 Do I see this right that people would set a url parameter for example to enable this fallback?
    • Assuming the adBlocker blocked the loading of matomo.js, and _paq was configured by the site, we could potentially read cookie parameters from there and interpret these without the user needing to set them. Not sure if this special case is often a thing though or not

Overall it sounds all good. It's bit hard to imagine the user experience so we might just need to tweak to make sure we have a good user experience should there be not a tracker on the same site (for whatever reason).

@bx80
Copy link
Contributor

bx80 commented Jun 13, 2022

Thanks for the feedback @tsteur 👍

✔️ Only wait 30secs for the JS tracker

Do I see this right that people would set a url parameter for example to enable this fallback?

Yes, I was thinking we have a URL parameter to enable the fallback to direct cookies, this would allow flexibility for unusual configurations where direct cookie writing isn't desired for some reason. Maybe it defaults to fallback enabled?

✔️ If the JS tracker isn't detected, fallback is enabled and there are no custom cookie settings in the URL params then attempt to load custom cookie settings from the _paq variable if it is set, else fall back to using the default cookie settings.

For honoring existing opt outs, maybe we check for an old third party cookie as part of the controller request, if it exists and there is no first party cookie then we use the third party cookie value as an initial opt out state and then immediately write the first party cookie to match it, but when we detect that a first party cookie exists then we just ignore the third party cookie. This would 'migrate' existing third party cookies to first party. Do you think that would work? 🤔

I think I have enough to get started, so I can try to implement the 'migrate' cookies approach if we think it will work and then we can test the user experience to see if anything needs to be tweaked.

@tsteur
Copy link
Member Author

tsteur commented Jun 13, 2022

This would 'migrate' existing third party cookies to first party. Do you think that would work?

It may work in some browsers. I'm not 100% sure how all the browsers handle third party cookies these days. Ignoring the third party cookie would mean effectively deleting the third party cookie in the controller request I assume. AFAIK at least Safari would block such a deletion but that could be fine (or maybe it would even work :) ). In the worst case a user would stay opted out which could be fine. It's mostly only an issue if a website owner previously opted out via 3rd party cookie and then they want to migrate the opt out and test if it works and then opting in and out again might not work. Maybe it all works nicely though and maybe we could also ignore this edge case.

@KarthikRaja1388
Copy link

Customer’s message:

During testing we have identified an issue regarding the opt-out iFrame message- see below. The iFrame message appears as it should do when viewed using a PC but when viewed on an Apple device (safari browser), user see the message in red below. It seems like apple software is automatically set to block cookies unless user opt-in via their devices privacy settings.

@Findus23
Copy link
Member

Findus23 commented Jun 16, 2022

Just a quick thought: The case where people have the matomo.js loading correctly sounds great to me, but I think we should change the other one: If matomo.js is blocked (let's assume the user has an ad-blocker enabled) and one wants to opt out. Even with 30s timeout, people would just assume that Matomo if forcing everyone to do the incredibly user-hostile "wait for this extremely long loading bar to move to opt out" that far too many websites are doing and go away with a bad impression of Matomo (while with the iFrame at the moment they would think that the opt-out is quite easy).
Also I am not sure why the fallback is something that needs to be enabled (or could be disabled)? Isn't this something that's a basic requirement for the opt-out to work for everyone? After all without it a large fraction of users could not opt out from tracking at all which goes against the point of the opt-out feature.

And this brings me to another issue: What to do if the optOutJs JS fails to load? After all this is executable code (often) from a third-part domain which also often contains matomo or piwik in the domain. And that means that this request would be blocked by most ad-blockers by default. (After all there is no technical difference between it and some tracking JS and no way to automatically know that it won't track the user anyway). So even with the above issues solved, still a large fraction of website users would just see no way to opt out and just assume that the website doesn't allow them/is not GDPR-compliant. With the iFrame the chance of it being blocked is far less likely and even in that case it would render as a broken block instead of an empty div.
Honestly I can't think of a way to solve this without increasing the complexity of the opt-out snipped. But at the very least the div should be pre-filled with a placeholder text (but then it would hard-code the language) explaining this fact.

@bx80
Copy link
Contributor

bx80 commented Jun 22, 2022

Some good points there @Findus23 👍

Even with 30s timeout, people would just assume that Matomo is forcing everyone to do the incredibly user-hostile "wait for this extremely long loading bar to move to opt out"

It's definitely not ideal, do you have an alternative approach to waiting 30 seconds to see if matomo.js loads? If we don't wait at all or wait for only a few seconds then the tracker code will be rarely used, at which point it would probably be better to ignore it altogether. For comparison the current iframe JavaScript will wait two minutes for matomo.js to load and then fail silently.

Also I am not sure why the fallback is something that needs to be enabled

The fallback option would be enabled by default, so the choice to disable it would only be there to provide flexibility for sites that need it. (Maybe matomo.js has been modified and it's not appropriate to set direct cookies? Or some other site config we haven't imagined?)

What to do if the optOutJs JS fails to load?

I suppose we could use an Ajax call instead of a direct <script> tag to load the optOutJs, this would at least allow us to detect if the request failed and show an appropriate message in the div, it does increase the complexity of the opt-out code quite a bit though and doesn't do anything to prevent the optOutJs script being blocked. Something like this:

<div id="m-opt-out"></div>
<script>
showOptOut('foo');
function showOptOut (id) {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
    if (this.readyState == 4 && this.status == 200) {
       eval(xhr.responseText); 
    } else {
       document.getElementById("m-opt-out").innerHTML = "failed to load tracking opt out";
    }
    xhr.open('GET', '/index.php?module=CoreAdminHome&action=optOut&language=en&backgroundColor=FFF');
    xhr.send();
}
</script>

But at the very least the div should be pre-filled with a placeholder text

We could add a placeholder to the generated code with some sort of hard coded loading message. The opt-out generator UI could provide an option for the user to set this message.

<div id="m-opt-out">... Tracking opt out is loading ...</div>

It seems like the only way to be sure that the opt-out will not be blocked would be to have the generated opt-out code be entirely self-contained, which is already covered by the alternative / custom option specified above.

If the self-contained code could be kept compact and fairly simple then I wonder whether it would be a better default option for the out-out generated code since it would be more reliable? The optOutJs version with a <script> tag compatible with the iFrame version could then be an easier upgrade option for existing opt out users (if a little less reliable).

@tsteur Do you have any thoughts?

@tsteur
Copy link
Member Author

tsteur commented Jun 22, 2022

I don't have any strong thoughts. I've seen some platforms offering self-contained code and they copy/paste the entire code into the page. The problem being that if there's any regression or browser incompatibility or other change then users will likely not know when they should update this self-contained code and it can often be complicated to get it changed. On the other side of course it won't be blocked.

For example if anything changes around cookies (which it did in the past) then the optOut might break.

Maybe if the OptOut JS is blocked, then users also wouldn't be tracked and it wouldn't be as much of an issue? Or maybe we could also get the path allowed in some privacy filters?

Generally I don't have a big preference though either way. I guess in the end we might just give people the option to either self contain the code (see https://developer.matomo.org/guides/tracking-javascript-guide#optional-creating-a-custom-opt-out-form) or use the code that loads a file?

@MatomoForumNotifications

This issue has been mentioned on Matomo forums. There might be relevant details there:

https://forum.matomo.org/t/opt-out-using-the-matomo-javascript-tracker-code-is-triggert-by-page-reload/47768/3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: Privacy For issues that impact or improve the privacy. Critical Indicates the severity of an issue is very critical and the issue has a very high priority.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants