18

I have Pageview tag in Google Tag Manager that tracks SPA pageviews, identical to the one described in this guide. Basically it is Universal Analytics with linked Google Analytics ID that is triggered on History Change (at some point All Pages trigger was also added with no success).

In my current app GTM skips Pageview tag on initial pageviews on all routes that don't have async resolvers. Usually the routes fire the tag sometimes (1 of 5 times), this may vary a bit depending on conditions (cached vs uncached, localhost vs production).

On the routes that have resolvers with long durations (> 1s) Pageview tag is always fired on initial pageviews (5 of 5 times).

Pageview tag is fired normally on all routes after app initialization (pushState).

This behaviour was confirmed with GTM debug console and GA realtime monitoring.

The setup seems to be the recommended one, GTM snippet is loaded in <head>, Angular 2 app is loaded at the end of <body>.

<html>
  <head>
    <script>/* Google Tag Manager snippet */</script>
    ...
  </head>

  <body my-app>
    ...
    <script src="my-app-bundle.js"></script>
  </body>
</html>

And Angular 2 is bootstrapped like usual:

platformBrowserDynamic().bootstrapModule(MyAppModule);

I've tried to move GTM snippet all around, before and after my-app-bundle.js, even replace it with synchronous:

<script>
  window.dataLayer = ...
</script>
<script src="https://www.googletagmanager.com/gtm.js?id=..."></script>

There was no difference with default snippet.

I've found by trial and error that Pageviews start to work normally on initial pageviews if the app is bootstrapped with considerable delay, 200-1000ms (it seemed at first that DOMContentLoaded does the trick but the delay wasn't enough):

setTimeout(() => {
  platformBrowserDynamic().bootstrapModule(MyAppModule);
}, 1000);

I hope that this problem is familiar to the experts who've done GTM with SPA/Angular 2 applications. Unfortunately, I cannot provide MCVE for this case but I believe it can be replicated with any Angular 2 (2.3.1) app with routing and Google Tag Manager account.

Usually Angular 2 apps can be safely bootstrapped at the end of <body>.

What is going on there and how pageview tracking should be properly handled with GTM without race conditions?


UPDATE: When switching from GTM to using GA directly with

router.events.subscribe(e => {
  if (e instanceof NavigationEnd)
    ga('send', 'pageview', location.pathname);
})

everything works fine on initial and subsequent pageviews with no race conditions.


UPDATE 2:

Here's a timeline of how it looks in the case of success with long-running route resolver, gtm.js and main.bundle.js are loaded in the beginning (it doesn't matter in which order), analytics.js (analytics_debug.js when GA Debugger is on) is loaded when route resolver completes and Pageview tag is fired, i.e. after ~10s:

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • I'm not sure if this would help in this instance; but the google-sign-in button must be manually rendered in NgAfterViewInit due to a very similar issue. Is there a manual function you could call in this library to trigger the behavior you need? – BradleyDotNET Jan 26 '17 at 01:31
  • @BradleyDotNET Unfortunately, no. It looks like A2 app should be adapted to fit GTM lifecycle, not in the opposite way (currently it looks like the app should be bootstrapped on window `load` to fit it). – Estus Flask Jan 26 '17 at 06:20

3 Answers3

1

As @kemsky suggested, GTM lifecycle is tied to internal gtm.load event, which happens on window onload. So DOMContentLoaded may be too early to bootstrap.

Considering that GTM script was loaded prior to SPA script,

window.addEventListener('load', () => {
  platformBrowserDynamic().bootstrapModule(MyAppModule);
});

callback will be triggered when GTM is ready to receive history change events, and there should be no race conditions.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • But this will delay app bootstrap. Any way to fix this without performance penalty ? – hex May 30 '17 at 13:56
  • @hex I guess it's one way or another. The other way would be hacking GTM source code, which is not an option. From my experience delay was negligible (had to switch to GA to avoid GTM overhead any way). Feel free to update with your measurements if you will have performance issues due to `window.addEventListener('load' ...)`. – Estus Flask May 30 '17 at 14:11
0

It looks like analytics scripts are loaded with <script async=true>:

j.async = true; or a.async = 1;

Try to remove async and see if it helps.

kemsky
  • 14,727
  • 3
  • 32
  • 51
  • 1
    It was already mentioned in the question, `even replace it with synchronous:`. – Estus Flask Jan 22 '17 at 22:17
  • You have tried with `gtm.js`, but in fact `gtm.js` loads `analytics.js` and it is also loaded async. it looks like you should either not use gtm or use `gtm.load` event to start application. – kemsky Jan 22 '17 at 22:46
  • I don't think that this is the case. It is hard to believe that GTM is that stupid. I've updated the question with a screenshot that confirms that it's not. GTM loads analytics.js on demand. The problem is not that GA is not loaded but the tag is not fired (it is seen in GTM debug bar). – Estus Flask Jan 23 '17 at 00:13
  • But you've mentioned interesting thing. Indeed, it looks like GTM differs `gtm.dom` (document load?) and `gtm.load` (window load?) events. This could possibly explain why I had no success with delaying the bootstrap with `DOMContentLoaded`. What do you mean by *use gtm.load event to start application*? – Estus Flask Jan 23 '17 at 00:45
  • previously i had experience with google charts, it works using the same pattern when one script is loading other and so on. You can delay start using `gtm.load` event instead of `domconententloaded`. Also google had issues with zone.js, so you may try to update zone, to load zone.js before or after gtm is loaded. – kemsky Jan 23 '17 at 01:26
  • Do you have specific information about `gtm.load` DOM event? `window.addEventListener('gtm.load'...` and `document.addEventListener('gtm.load'...` listeners aren't triggered. I guess I've already excluded zone.js from the equation, I've tried to load GTM as the first and the last script on the page. – Estus Flask Jan 23 '17 at 01:40
  • It looks like `gtm.load` fires on window `load` event (not sure what happens there, but it looks like it is a crucial moment). So considering that gtm.js is executed before JS file where SPA is initialized, bootstrapping on window `load` should be a solution. It was your idea in the first place, and I guess it can be accepted as an answer. – Estus Flask Jan 24 '17 at 20:46
0

You may change the trigger of your UA pageview tag from plain 'pageview' and stick to custom event fired at NavigationEnd

router.events.subscribe(e => {
  if (e instanceof NavigationEnd) {
    dataLayer = window.dataL
    dataLayer.push({'event':'custom pageview event'});
  }
})
Дмитро Булах
  • 3,697
  • 1
  • 14
  • 23