4

My problem in a nutshell: The window object that gtag.js operates on and the window object available in my react context (a content.js context) are different objects, and so I can't write events from my react code -- meaning I can't use analytics in my extension.

More deets:

In react <script> tags can't be loaded directly for various reasons. So I've change the documentation implementation:

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'GA_MEASUREMENT_ID');
</script>

To

export const gtag = (...args) => {
  window.dataLayer.push(args)
}

export const loadAnalytics = (ga_property) => {
  const script      = windowdocument.createElement("script")
  script.src        = `https://www.googletagmanager.com/gtag/js?id=${ga_property}`
  script.async      = true
  window.document.body.appendChild(script)
  window.dataLayer = window.dataLayer || []
  gtag('js', new Date())
  gtag('config', ga_property, { 'transport_type': 'beacon'})
  gtag('event', 
      'test', {
      event_category: 'lookup',
      event_label: 'test'
    }
  )
}

...

  componentDidMount() {
    loadAnalytics("UA-175XXXXXX-1")
  }

I've come to understand through much research and gnashing of teeth that the window object in my content.js and the window object that is acted on in gtag.js once it is loaded are not the same object, and are intentionally "shadows" of each other, but still separate objects. From the documentation:

"Content scripts live in an isolated world, allowing a content script to makes changes to its JavaScript environment without conflicting with the page or additional content scripts.

Isolated worlds do not allow for content scripts, the extension, and the web page to access any variables or functions created by the others."

From what I can tell this seems to be irreconcilable without a re-write of the gtag.js source.

For reasons I still don't understand this code which references window.document

  const script      = window.document.createElement("script")
  script.src        = `https://www.googletagmanager.com/gtag/js?id=${ga_property}`
  script.async      = true
  window.document.body.appendChild(script)

And this code in the same file which references window.document

export const gtag = (...args) => {
  window.dataLayer.push(args)
}

End up pointing to two different window objects.

This post seems to reinforce that these two contexts can't communicated directly in terms of objects and functions (only messages).

For gtag.js to work in an extension, I'd need to be able to call window.dataLayer.push(...) on the window of the main web page from inside my chrome extension. And that doesn't seem possible.

Any bright ideas out there as to how to either:

  1. Make gtag.js be loaded in the proper window.document and/or refer to the content.js context of window or
  2. be able to access the window object of the main page from the content.js context
pixelearth
  • 13,674
  • 10
  • 62
  • 110
  • Content script is isolated from webpage for security concerns. Can you please explain why need to use `window.dataLayer.push(...)` of the webpage? Does the webpage belong to you? – Aefits Aug 21 '20 at 09:38
  • @elegant-user this is how gtag.js works, it pushes to that object. In its setup code it defines a function that accesses that, and as a user I use that function to publish events (this is in the code I shared): function gtag(){dataLayer.push(arguments);}, gtag('js', new Date()); – pixelearth Aug 21 '20 at 17:42
  • Just to be clear, which events are you trying to track using analytics? If it's events in the UI of your extension, that's easy. So I'm assuming you want to inject a google analytics script into any page via content scripts? My guess is that you would have to handle this using messaging, from content script via a background script relay into your React UI – Tom Aug 24 '20 at 07:27
  • @Tom it's just events in the extension. The issue is that when the gtag.js loads, it refers to window.document internally, but the window.document it has access to is different from the one that my content script has access to. So I can't record any events at all. – pixelearth Aug 24 '20 at 17:39
  • So content scripts have their own world, with access to the DOM but without access to the javascript environment of the loaded page they have been injected into. When you add a script tag, in the content script, you will be adding to the DOM and the execution environment of that script tag will belong to the page not your content script world. If you injected the tag manager script as a content script, would that not work? – Tom Aug 25 '20 at 18:56
  • Or. alternatively, create a script element from your content script code and add that to the DOM. – Tom Aug 25 '20 at 19:01
  • Here's a very good discussion of the various ways of doing it (first answer): https://stackoverflow.com/questions/9515704/insert-code-into-the-page-context-using-a-content-script – Tom Aug 25 '20 at 19:02
  • @Tom The `gtag.js` script is loading from within the content script, yet it executes itself in the context of the main page. Check out the part of my post that starts "For reasons I still don't understand this code which references window.document..." – pixelearth Aug 25 '20 at 19:03
  • I was trying to say that when you add a script with a script tag, and attach it to the body of the document, it automatically executes in the 'main world', not the content script world. – Tom Aug 25 '20 at 19:07
  • Anyway, that post might give you some possible routes – Tom Aug 25 '20 at 19:08
  • @Tom It will prob be confusing trying to explain, but I hear what you're saying. The thing is, the `window` object used in my content script should only refer to the window of the content script (the isolated env). So when I write `window.document.body.appendChild(script)` it should only attach to that. The weird thing is that it actually attaches to both envs, which I can see in the console. When you refer to `window.document` you get the "proper" `document`, but the `gtag.js` internals simply refer to `document` not prefixed by `window`, and I think this is why it "gets" the "wrong" env. – pixelearth Aug 25 '20 at 19:28

1 Answers1

0

Since extension code can have multiple contexts, it would be wise use the principle of separation of concerns to avoid multiple document issue altogether.

When developing extensions it is advised to run majority of your code in the background, to make use of the separate JavaScript runtime allocated for your code by the browser (and avoid slowing down the pages user is visiting or the code which appears as your extension UI). Additionally, in most cases, it is a good idea to ship the code you want to run packaged within the extension bundle. If you want to load an external resource, to your background script "document", you can use XHR and eval to execute code.

When code is executed in the background, it is available to your extension UI and content scripts using the extension and DOM messaging protocols.

  1. First, initialize your extension in the context of your background script(s).
  2. Then, register a message handler which will evaluate messages sent by other extension code and look for a key (usually message.type) that identifies message as carrying analytics data (usually message.payload).
  3. Read the content of the messages that match the criteria in the handler, and use the supplied information to invoke analytics APIs.
  4. Finally, send analytics events occurring in your UI or content scripts as messages to the background script.

This way your background script is the only place where analytics is set up, clearing up document ambiguity, your code is cleaner, because there is only one place where analytics code is accessed and the extension runs smoother because it's UI and content scripts don't need to load or know about analytics code.

ermik
  • 408
  • 3
  • 17
  • You're offering a general suggestion to a specific problem. That in itself is often enough to get down-voted on Stack Overflow. The issue involves using `gtag.js` in an extension. Have you actually used this in an extension before and have you tried it from the background script? Are you suggesting that I package it with my extension? How do you reconcile the fact that analytics tracks hits but the background page is loaded once per browser session? – pixelearth Aug 28 '20 at 00:09
  • It sounds like you're suggesting that I track all events manually via the messaging protocols. I can actually already track events manually (with no need for the messaging approach) via "google analytics measurement protocol", I was just hoping to get more mileage and simplicity out of the `gtag.js`. – pixelearth Aug 28 '20 at 00:10
  • I appreciate your feedback and your votes, whether up or down. While I understand my contribution wasn't helpful, I do sincerely hope you will solve this problem. Please, if you could, share your solution if you find it elsewhere so that other community members can learn from it. Thank you! – ermik Aug 30 '20 at 15:48
  • I will try to address your questions to the best of my understanding. I am suggesting you initialize `gtag.js` once, in the background, and "wrap" any method invocations which reference it in functions which would use extension-specific messaging protocols to send data to the background context which then calls `gtag` methods. The functions using extension messaging to relay analytics payloads to background would have no knowledge of `gtag` decoupling your analytics layer and the analytics provider. Yes, I have implemented this approach before. I hope this was helpful. Thanks again! – ermik Aug 30 '20 at 15:58