Angular and Google Analytics

Viewed 381

I'm trying to add Google Analytics to my Angular app without having to add the scripts manually to the index file. My goal is to use a service that has functions you can call to initialize the analytics and track page views. Here's the main part of the service:

export class GoogleAnalyticsService {
    private googleAnalyticsId: string;
    private renderer2: Renderer2;
    private scriptsLoaded: boolean = false;

    constructor(
        private rendererFactory2: RendererFactory2,
        @Inject(DOCUMENT) private _document: Document,
        private _config: RuntimeConfigLoaderService,
        private _router: Router,
    ) {
        this.renderer2 = this.rendererFactory2.createRenderer(null, null);
        this.googleAnalyticsId = this._config.getConfigObjectKey('googleAnalyticsId');
    }

    init() {
        this.insertMainScript();
    }

    private insertMainScript() {
        if (this.googleAnalyticsId) {
            const script: HTMLScriptElement = this.renderer2.createElement('script');
            script.type = 'text/javascript';
            script.onload = this.insertSecondHalfOfScript.bind(this);
            script.src = `https://www.googletagmanager.com/gtag/js?id=${this.googleAnalyticsId}`;
            script.text = '';
            this.renderer2.appendChild(this._document.body, script);
        }
    }

    private insertSecondHalfOfScript() {
        const script: HTMLScriptElement = this.renderer2.createElement('script');
        script.type = 'text/javascript';
        script.src = '';
        script.text = `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
        `;
        this.renderer2.appendChild(this._document.body, script);
        this.scriptsLoaded = true;
    }

    trackSinglePageView(event: NavigationEnd) {
        if (this.googleAnalyticsId && this.scriptsLoaded) {
            console.log('logging pageview');
            gtag('config', this.googleAnalyticsId, { page_path: event.urlAfterRedirects });
        }
    }

    trackPageViews() {
        return this._router.events.pipe(
            filter((evt: RouterEvent) => evt instanceof NavigationEnd),
            tap((event: NavigationEnd) => {
                this.trackSinglePageView(event);
            }),
        );
    }
}

The main function to call from the AppComponent is init. That will inject the scripts onto the page. As I watch the application load, I see those scripts get added. I also see the https://www.googletagmanager.com/gtag/js script load in the Network tab of the developer tools. So the script is definitely there.

Then after calling the init function in the AppComponent I call the trackPageViews function and subscribe to the returned Observable. I filter out all the router events until the above scripts are loaded, and then filter out all events that are not NavigationEnd events. If the scripts are loaded and it's a NavigationEnd event, then the trackSinglePageView function and gtag is called with the necessary data. However, when this happens, I get the following error:

core.js:6189 ERROR ReferenceError: gtag is not defined
    at GoogleAnalyticsService.trackSinglePageView (google-analytics.service.ts:58)
    at TapSubscriber._tapNext (google-analytics.service.ts:69)
    at TapSubscriber._next (tap.js:40)
    at TapSubscriber.next (Subscriber.js:49)
    at FilterSubscriber._next (filter.js:33)
    at FilterSubscriber.next (Subscriber.js:49)
    at Subject.next (Subject.js:39)
    at SafeSubscriber._next (router.js:7593)
    at SafeSubscriber.__tryOrUnsub (Subscriber.js:183)
    at SafeSubscriber.next (Subscriber.js:122)

If I don't use the service to load the analytics scripts and put them in the head of the app's index.html file and just track the page views, then I don't get any errors in the console. So it appears that although the scripts are loaded, gtag is still not defined unless the analytics scripts are in the head.

Is there any way to get around this? Am I doing something incorrect here? Any ideas greatly appreciated.

2 Answers

So it turns out that the text of the script created in the insertSecondHalfOfScript function was never run. It seems like that's where gtag is made available for the page. So instead of adding the code to that text attribute, I put it in an asset file and loaded it like I load the script in the insertMainScript function. So the initiation flow looks like this now:

init() {
    this.insertMainScript();
}

private insertMainScript() {
    if (this.googleAnalyticsId) {
        const script: HTMLScriptElement = this.renderer2.createElement('script');
        script.type = 'text/javascript';
        script.onload = this.insertSecondHalfOfScript.bind(this);
        script.src = `https://www.googletagmanager.com/gtag/js?id=${this.googleAnalyticsId}`;
        script.text = '';
        this.renderer2.appendChild(this._document.body, script);
    }
}

private insertSecondHalfOfScript() {
    const script: HTMLScriptElement = this.renderer2.createElement('script');
    script.type = 'text/javascript';
    script.src = '/assets/scripts/analytics/analytics-starting-script.js';
    script.text = '';
    this.renderer2.appendChild(this._document.body, script);
    script.onload = () => {
        this.scriptsLoaded = true;
    };
}

Loading the second script like this seems to work, as I now don't get the ReferenceError I was getting before.

I know we did our gtag implementation by having a separate index file for dev vs prod.

You can configure you angular.json to use one or the other

  "configurations": {
        "production": {
          "fileReplacements": [
            {
              "replace": "src/environments/environment.ts",
              "with": "src/environments/environment.prod.ts"
            }
          ],
          "index": {
            "input": "src/overrides/prod/index.prod.html",
            "output": "index.html"
          },
         "dev": {
          "fileReplacements": [
            {
              "replace": "src/environments/environment.ts",
              "with": "src/environments/environment.dev.ts"
            }
          ],
          "index": {
            "input": "src/overrides/qa/index.qa.html",
            "output": "index.html"
          },
Related