Bonus: And missing page visits.

Introduction

This article includes all the steps to create a Data Studio report on the errors that happen on your users browsers. It requires a bit of coding (GTM or gtag), a GA4 property and some patience in setting things up.

Website Errors Report
Website Javascript Errors Report
JavaScript errors including where they happened

The code

This code can be used when inplementing GA4 via gtag or GTM. The next sections explain how to add it via each method.

<script>
    var useGTM = false; // if false uses gtag
    /*
        Web Site Advantage: Javascript and resource loading error tracking for GA4 [v2.1]
        https://bigcommerce.websiteadvantage.com.au/error-tracking-with-ga4/
    */

    function sendGa4Event(event, options) {
        
        window.dataLayer = window.dataLayer || [];

        if (useGTM) {     
            // send the GA4 event via the dataLayer
            dataLayer.push({
                event: event,
                error: options
            });
        } else {
            // Ensure gtag is made present so that this code can be placed before the gtag code
            if (!window.gtag) { 
                window.gtag = function gtag(){dataLayer.push(arguments);}
            }
        
            gtag('event', event, options);
        }
    }

    // helper to add event listeners to elements in a cross browser way
    function addOrAttachEventListener(target, type, listener, options) {
        if (target.addEventListener) {               // For all major browsers, except IE 8 and earlier
            target.addEventListener(type, listener, options);
        } else if (element.attachEvent) {               // For IE 8 and earlier versions
            target.attachEvent('on'+type, listener);
        }
    }

    // GTM has a built in JavaScript Error Listener
    if (typeof window.onerror == "object") { // test to see if the onerror event exists

        var onErrorHandling = false; 

        addOrAttachEventListener(window, 'error', function(messageOrEvent, filename, lineno, colno, error) { 
          
            if (!onErrorHandling) {  // so error handling does not cause a recursive loop

                try {
                    onErrorHandling = true;

                    if(messageOrEvent.target && messageOrEvent.target !== window && messageOrEvent.target.nodeName){
                        // it's a resource loading error.

                        var target = messageOrEvent.target;
                                            
                        var targetUrl = target.src || target.href;  // url is in either a src or href attributes    

                        var error_options = {
                            error_type: 'network',
                            error_message: 'Load ' + target.nodeName + ' tag error', 
                            error_object_type: target.nodeName,
                            error_filename: targetUrl,
                            fatal: false  
                        };

                        error_options.description = error_options.message + ' ' + targetUrl;

                        sendGa4Event('exception', error_options);    

                    } else {
                        // it's a Javascript error
                    
                        var error_options = {
                            error_type: 'javascript', 
                            error_object_type: "Unknown",
                            error_message: "Unknown", 
                            description: 'Javascript',
                            fatal: false
                        };

                        if (messageOrEvent) {
                            if (typeof messageOrEvent === 'string') {
                                error_options.error_message = messageOrEvent;
                                error_options.error_object_type = "Message";

                            } else {   
                                                   
                                error_options.error_filename = messageOrEvent.filename;
                                error_options.error_lineno = messageOrEvent.lineno;
                                error_options.error_colno = messageOrEvent.colno;
                                error_options.error_error = messageOrEvent.error;
                                error_options.error_message = messageOrEvent.message;

                                if (messageOrEvent.filename) { // it's an event
                                    error_options.error_object_type = "Event";    

                                } else if (messageOrEvent.originalEvent) { // it's been intercepted

                                    error_options.error_object_type = "Intercepted Event";  

                                    error_options.error_filename = error_options.error_filename || messageOrEvent.originalEvent.filename;
                                    error_options.error_lineno = error_options.error_lineno || messageOrEvent.originalEvent.lineno;
                                    error_options.error_colno = error_options.error_colno || messageOrEvent.originalEvent.colno;
                                    error_options.error_error = error_options.error_error || messageOrEvent.originalEvent.error;
                                    error_options.error_message = error_options.error_message || messageOrEvent.originalEvent.message;    

                                } else {
                                    error_options.error_object_type = "Object";  
                                    error_options.error_message = JSON.stringify(messageOrEvent, Object.getOwnPropertyNames(messageOrEvent)); // attempt to get properties that normally don't get included
                                }
                            }
                        }
                    
                        // create the description, a summary of the error in one line, useful for realtime checking
                        if (error_options.error_filename) error_options.description += ': ' + error_options.error_filename;

                        if (error_options.error_lineno) {
                            error_options.description += ': L' + error_options.error_lineno;
                            if (error_options.error_colno) {
                                error_options.description += ' C' + error_options.error_colno;
                            }
                        }
                        if (error_options.error_message) error_options.description += ': ' + error_options.error_message;

                        // make sure error_error is a string
                        if (error_options.error_error && typeof error_options.error_error !== 'string') {
                            error_options.error_error = JSON.stringify(error_options.error_error, Object.getOwnPropertyNames(error_options.error_error));
                        }

                        // dump the error object into error_data
                        if (messageOrEvent && typeof messageOrEvent !== 'string') {
                            error_options.error_data = JSON.stringify(messageOrEvent, Object.getOwnPropertyNames(messageOrEvent));
                        }

                        sendGa4Event('exception', error_options);
                    }
                }
                catch (err) {
                    console.log("OnErrorHandling ERROR: ",err);

                    sendGa4Event('exception', {
                        error_type: 'javascript', 
                        error_object_type: "Handler ERROR",
                        error_message: "The error processor had an error!", 
                        description: 'The error processor had an error!',
                        fatal: false
                    });
                }
                finally {
                    onErrorHandling = false;
                };
            }
            return false; // let the default handler do its job
        }, {
            passive:true, // passive means does not call preventDefault(). Faster
            capture: true // capture so it gets errors which don't make it to the top in the bubble phase. e.g. resource load errors
        }); 
    }

    // track console errors
    console.error_previous = console.error;
    console.error = function() {
        console.error_previous.apply(console, arguments); // make sure it still outputs the error

        sendGa4Event('exception', {
            error_type: 'console', 
            error_message: '' + arguments[0], 
            description: 'Console: ' + arguments[0],
            fatal: false
        });
    };

    // track XMLHttpRequest
    if(window.XMLHttpRequest && window.XMLHttpRequest.prototype) { 

        var prototype = window.XMLHttpRequest.prototype;

        if(prototype.send && prototype.send.apply) {
            prototype.send_previous = prototype.send;

            prototype.send = function() {
                var xmlHttpRequest = this;

                addOrAttachEventListener(this, 'readystatechange', function(){ 
                    try {
                        if (xmlHttpRequest.readyState == 4) { 
                            if (xmlHttpRequest.status >= 400 || xmlHttpRequest.status === 0) { // 0 includes cors errors
                                sendGa4Event('exception', {
                                    error_type: 'network', 
                                    error_message: xmlHttpRequest.status, 
                                    error_filename: xmlHttpRequest.responseURL,
                                    description: 'XMLHttpRequest Response: ' + xmlHttpRequest.status + ': ' + xmlHttpRequest.responseURL,
                                    fatal: false
                                });
                            };
                        };
                    } catch(err) {} // don't want our code causing anything to fail
                });
                return prototype.send_previous.apply(this, arguments)
            };
        };
    };


    // track fetch requests
    if(window.fetch && window.fetch.apply){
        window.fetch_previous = window.fetch;

        window.fetch = function(url) {
            return window.fetch_previous.apply(this,arguments)
            .then(function(response){
                try {
                    if(response.status >= 400){                   
                        sendGa4Event('exception', {
                            error_type: 'network', 
                            error_message: response.status, 
                            error_filename: response.url,
                            description: 'Fetch Response: ' + response.status + ': ' + response.url,
                            fatal: false
                        });
                    };
                } catch(err) {} // don't want our code causing anything to fail

                return response;
            })
            .catch(function (error) {
                try {
                    sendGa4Event('exception', {
                        error_type: 'network',
                        error_message: error,
                        error_filename: url,
                        description: 'Fetch Error: ' + error,
                        fatal: false
                    });

                } catch (err) { } // don't want our code causing anything to fail

                throw error; // propagate the exception 
            });
        };
    };

    // track beacons
    if(navigator && navigator.sendBeacon) {
        navigator.sendBeacon_previous = navigator.sendBeacon;

        navigator.sendBeacon = function(url, data) {
            navigator.sendBeacon_previous.apply(this, arguments);

            var size = data && data.length || 0;

            // GA4 beacons have a limit of 16*1024 (16384)
        };
    };

    // track json-ld syntax errors
    setTimeout(function() {
        var jsonLdScripts = document.querySelectorAll("script[type='application/ld+json']");

        for(var i=0; i<jsonLdScripts.length; i++){
            var jsonLdScriptText = jsonLdScripts[i].text.trim();

            if(jsonLdScriptText !== '') { // consider a blank one as fine
                try {
                    JSON.parse(jsonLdScriptText);
                }
                catch(err) {
                    sendGa4Event('exception', {
                        error_type: 'jsonld', 
                        error_message: ''+err, 
                        description: 'JSON-LD: '+err,
                        fatal: false
                    });
                }
            }
        }

    }, 5000); // give the page 5 seconds to load any structured data
</script>

Adding the code to your site (for GA4 using gtag)

Place the above code as near to the top of your html as possible. This gives it a better chance to catch early errors.

We also recommend tracking page types. This can help you group your errors into sections of the site. You will have to devise your own way of identifying page types, than add it to your gtag code like this:

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

  gtag('config', 'G-XXXXXXXXXX', {
    page_type: 'PAGE TYPE NAME'
 });
</script>

Adding the code to your site (for GA4 using GTM)

Take the code above and change the useGTM value at the top to true. Add the code as a Custom HTML tag that fires on All pages. Call it something like “Error Tracking”.

GTM Error Tracking Tag
GTM Error Tracking Tag

We then have to add the following Data Layer Variables:

Clicking on a cell will copy its content to your clipboard.

NameData Layer Variable Name
DLV – error.error_typeerror.error_type
DLV – error.error_messageerror.error_message
DLV – error.error_object_typeerror.error_object_type
DLV – error.descriptionerror.description
DLV – error.fatalerror.fatal
DLV – error.error_filenameerror.error_filename
DLV – error.error_linenoerror.error_lineno
DLV – error.error_colnoerror.error_colno
DLV – error.error_errorerror.error_error
Data layer variables for error tracking

Next we need the trigger for these errors.

Name: Event – exception

Trigger Type: Custom Event

Event name: exception

Exception trigger
Exception trigger

Finally we need to create a GA4 Event tag that sends an exception event with all the parameters:

Name: GA4 – Event – exception

Tag Type: Google Analytics GA4 Event

Configuration Tag: Your own configuration

Event Name: exception

Trigger: Event – exception

Parameters:

Parameter NameValue
error_type{{DLV – error.error_type}}
error_message{{DLV – error.error_message}}
error_object_type{{DLV – error.error_object_type}}
fatal{{DLV – error.fatal}}
error_filename{{DLV – error.error_filename}}
error_lineno{{DLV – error.error_lineno}}
error_colno{{DLV – error.error_colno}}
error_error{{DLV – error.error_error}}
description{{DLV – error.description}}
exception event parameters
GA4 Event tag that sends an exception event
GA4 Event tag that sends an exception event

Like with the gtag implementation we recommend sending a page_type parameter to GA4 so that you can segment your reports. How you determine the value of the page_type is down to you. You will then have to send it in the dataLayer, create a variable for it, and add it to your GA4 Configuration fields.

GA4 Configuration Page Type

Adding GA4 definitions

You need to define all our dimensions and metrics that Data Studio uses before it can see them. Add them to your GA4 Account in the ‘Custom definitions’ section as dimensions of scope ‘Event’. Ones used by the Data Studio report are required.

Dimension NameDescriptionEvent ParameterRequired?
Page typeA way to group pages. example page types could be category, blog_posting, home.page_typeYes
DescriptionUsed by the exception event to explain the error. Tends to be a verbose version covering other dimensions. Good for real time inspectiondescriptionNo
Error typeUsed by the exception event to explain the type of error (javascript, console, jsonld, network)error_typeYes
Error messageThe explanation for an exceptionerror_messageYes
Error filenameUsed by the exception event to provide the file where the error happenederror_filenameYes
Error line numberUsed by the JavaScript exception event to provide the line where the error happenederror_linenoYes
Error column numberUsed by the exception event to provide the column in the line where the error happenederror_colnoYes
Error errorThe error property within the error eventerror_errorNo
Error object typeIdentifies the type of error object that was fired. error_object_typeNo

This could be a good time to check if errors are being reported. In the GA4 realtime report you can look for ‘exception’ events. If you click on the event it will show what parameters got sent. And if you click on them you can see the values. After a few days you should see them in the events report.

Creating the Reports Data Source

At the moment the standard Data Source for GA4 is not usable. It randomly messes up the custom definitions. We have worked out the following steps to create a special Data Source that can be used in our reports. Hopefully GA4/Data Studio fix this in the near future.

Data Source Fixing Functions
  1. Copy the Website Errors GA4 Data Source by opening the data source and clicking on the copy icon and the copy button
  2. Select your GA4 account from the connection options, then click the ‘Reconnect’ button. The new data source is now based on your property. A popup may appear indicating issues. Ignore them and click ‘Apply’
  3. Rename it (top left) to include your stores name. That way you know which store and which version of the data source you are looking at. e.g. ‘Website Errors GA4 – My Site – vX.Y’
  4. Sometimes Data Studio duplicates some of the inbuilt fields with only one of the copies working. If you see any invalid formula warnings then is is probably the case. Check through all the fields and add a ‘ 2’ after the make of duplicates. This will then let you try each duplicate in any failing formula.
  5. You now need to fix all the custom definition based fields so the data source works. Check every field starting with an @ (indicates it uses custom definitions) and make sure all formulas match the formulas in following table. If the formula fails then you may have to try the other duplicate for the referenced field.
  6. Change the @Missing Page formula to one that will work for your site. e.g. Page title = ‘Missing Page’
Field NameField IDFormula (fx)
@Error column numberfx_error_colnoError column number
@Error filenamefx_error_filenameError filename
@Error line numberfx_error_linenoError line number
@Error messagefx_error_messageError message
@Error typefx_error_typeError type
@Missing Pagefx_missing_page@Page type = ‘404’
@Page locationfx_page_locationPage location
@Page referrerfx_page_referrerPage referrer
@Page typefx_page_typePage type
Fields that need their formulas checking

The custom data source also contains the following extra fields that do not need to be checked. These fields are added so that a completely different connector could be used as the source for the report. e.g. BigQuery.

Field NameField IDFormula (fx)
#Browserfx_browserBrowser
Datefx_dateDate
Devicefx_deviceDevice
Device categoryfx_device_categoryDevice category
Event namefx_event_nameEvent name
Operating systemfx_operating_systemOperating system
Operating system with versionfx_operating_system_with_versionOperating system with version
Page path and query stringfx_page_path_queryPage path + query string
Session mediumfx_session_mediumSession medium
Session sourcefx_session_sourceSession source
Fields to enable switching of connectors

Reports will have errors or be inaccurate if the functions on those fields are incorrect.

Your data source is now ready to use to power new reports.

Creating the Report

Open the Website Errors report template and click on the ‘use template’ button. Select the data source you have previously created and click ‘Copy Report’. Rename the new report (top left) to include your site name. e.g. ‘Website Errors – My Site – vX.Y’