Trouble implementing Google Analytics gtag.js in React-based Chrome Extension

Viewed 1513

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
1 Answers

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.

Related