Script added dynamically to head: is it executed again during cache restoration?

I have a Stimulus controller like this for a cookie consent banner that, when accepted, executes some tracking code (Google Ads conversions):

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ['banner']
  
  connect() {
    if (localStorage.getItem('consent') === 'true') {
      this._tracking()
    } else {
      // display consent banner, etc ...
   }
  }
  
  _tracking() {
    var script = document.createElement('script')
    script.src = 'https://www.googletagmanager.com/gtag/js?id=AW-12345'
    script.async = true
    document.head.appendChild(script)
    gtag('set', { 'anonymize_ip': true })
    gtag('js', new Date())
    gtag('config', 'AW-12345')
  }
}

What will happen to that script (gtag) added dynamically to the head by the _tracking() function?

Will the code of that external script (https://www.googletagmanager.com/gtag/js?id=AW-12345) execute again during the cache restoration? And then again, after a few ms, when the actual page is loaded?

Or during cache restoration (those few ms of preview) the external script is not executed?

Just a naive read of it says that you may end up with a bunch of these script tags, one after another, and the last one will “win”. Maybe you want to write that method to be idempotent, either by using local storage to set a key when you complete adding the script, or just to look to see if that script already exists before you try to throw another one into the head.

That Stimulus _tracking() is executed by connect… and the controller is connected to a div (cookie consent banner) that is present on every page. That div has a data-turbo-temporary annotation, so I think that everything is correct.

My doubt is not about _tracking() being executed too many times… That is correct.

My doubt is about the external script (https://www.googletagmanager.com/gtag/js?id=AW-12345) that is (probably) executed twice by Turbo: e.g. during an “application visit” turbo first restores the HTML from cache and then loads the real page. Does this mean two execution of the script?

That’s a good question. Is there any way you can test this? Could you set up a stunt page that includes a link to another page, and then see how many hits that page gets when you visit it, leave, and then use the back button to navigate back?

It’s exactly what I was trying now :slight_smile: we had the same idea.

I made an example.js script with only this line of code and put it in the public directory (for simplicity):

console.log('example: current page: ', window.location.href)

Then I replaced https://www.googletagmanager.com/gtag/js?id=AW-12345 with /example.js in the above code and I check the console output.

I get the correct result. Only one execution per page.

If I visit a page again the example.js is not executed during the preview, only during the actual page load (I can simulate that adding sleep 5 to controller).

Perfect…

Oops… However if I add an event listener to example.js:

document.addEventListener("click", function() {
  console.log('EXAMPLE: click!')
});

That fires multiple times…

  • if I visit 1 page, 1 click produces 1 output to console
  • if I visit 2 pages, 1 click produces 2 outputs to console
  • if I visit 3 pages, 1 click produces 3 output to console

I need to find a way to fix this.

Turbo is a real mess for external scripts like gtag that you don’t control…

I can add this condition to the tracking() function to load the external script only if it is not included yet:

if (!document.querySelector('script[src="' + gtagScriptSrc + '"]')) {
  // load gtag script
}

… at this point, with this condition, the EXAMPLE: click! is printed only once (correct).

However the example: current page: output is now broken: it doesn’t show every page visited.

:frowning: :frowning:

For your event listener, you’re setting that up in Stimulus’s connect(), right? Try tearing it back down on the disconnect(). That way you control how many of them are there, and the connect firing is an event you can rely on. There is also an initialize() (I think) method in a Stimulus controller which can be relied on to only load once per … something. I honestly don’t know 100% if the Turbo and Stimulus parts talk to each other in this area.

Thanks for the input. Unfortunately I cannot just “remove” a script in disconnect. I mean, I could remove the script tag, but that will not remove all the events attached by that external script with addEventListener()

Sigh. Yes, you are correct. I am curious whether this is going to work out without some kind of affordance from Turbo. Those event handlers are going to hang around (and get re-added, too) because if all goes well, Turbo leaves the <head> alone.

Have you already tried and discounted something like this?

_tracking() {
    var script = document.createElement('script')
    script.src = 'https://www.googletagmanager.com/gtag/js?id=AW-12345'
    script.async = true
    script['data-turbo-track'] = true // <-- or one of the other possible control keys
    document.head.appendChild(script)
    gtag('set', { 'anonymize_ip': true })
    gtag('js', new Date())
    gtag('config', 'AW-12345')
  }

I’d be curious if you could go back to your original version and add that, what might happen. There are a few other control settings possible, and while this wouldn’t necessarily do anything about the “adding it twice” problem, it might just.

For my understanding, that command simply apply a full page reload if the external script changes, which is not my case.