How to Implement Google Tag Manager in Svelte, in Functional Programming

First of all, can Google start handling our data with care. And here’s how I implemented Google Tag Manager (GTM) to a Sapper-site. The regular script from GTM gave errors in this specific Sapper setup and that was my excuse to implement it with functional programming concepts.

Code is also in this gist

Post meta:

Disclaimer: Perhaps obvious, but I’m far from an expert on GTM, or functional programming (FP) for that matter.

Update 21.02.24: When using this code for a project I added the possibility of timing out loading the GTM script. On FP, I would now have coded this using const to declare functions for immutability—but hoisting would be considered.

Invoking GTM is an interesting case for how to do functional programming in Javascript. It includes the necessity of using global variables and dynamically loading a script.

Component Implementation—(Near) Full FP

Update 2021.01.26: GTM component in FP. Update 2021.01.29: Use <svelte:head>.

This is a Svelte component implementation with a pure, higher-order function that returns a function. This implementation works with Google Analytics 4 (GA4).

What counts is pure. The function callback of onMount could have side effects. It uses the mutable variable document, and it changes the DOM (if that counts as “outside” of a function). Everything else is pure.

Higher order function. The function returns a function, enabling currying.

[First-class functions] means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.

Wikipedia

Immutability, except the callback. All variables are immutable except the global variable document in onMount callback. No Array.prototype.push(): pushing to an array mutates the original array. I’ve used concat and the spread operator.

Code

Import component in Svelte and use:

<GTM {gtmId} {gtmDataPoints} />

And for the component (GTM.svelte):

<script>
    import { onMount } from 'svelte';

    /** @type {string} gtmId - GTM ID 'GTM-F00BARS'. */
    export let gtmId = '';
    /** @type {(Object[]|Object)} [gtmDataPoints=[]] - Array or single object of custom data points for dataLayer.
      * @type {Object} [gtmDataPoints[]] - Custom data point Object.
      * @type {string} [gtmDataPoints[][]] - Custom data point property. */
    export let gtmDataPoints = [];
    /** @type {number} [timeout=0] - The number of milliseconds to timeout intiating loading the GTM script from Google */
    export let timeout = 0;
    /** @type {boolean} [dev=false] - Set to true to give errors */
    export let dev = false;

    let scriptSrc;

    /** getFunctionScriptElementFromInitGtm - Sets global dataLayer on Window Object.
      * @param {(Object[]|Object)} [customDataPoints=[]] - Array or single object of custom data points for dataLayer.
      * @param {Object} [customDataPoints[]] - Custom data point Object.
      * @param {string} [customDataPoints[][]] - Custom data point property.
      * @param {Object} [globalObject=window] – E.g. a reference to the Window Object (window).
      * @returns {getFunctionScriptElementFromInitGtm~getScriptSrcForGtm} - function. */
    function getScriptSrcFromInitGtm ( customDataPoints = [], globalObject = window ) {
        const requiredDataPoint = {
            'gtm.start': new Date().getTime(),
            event: 'gtm.js',
        };

        /** getScriptSrcForGtm - Returns script src.
         *  @param {string} gtmId - GTM ID 'GTM-F00BARS'.
         *  @returns {string} - Src for script element. */
        function getScriptSrcForGtm ( gtmId ) {
            if (!dev && (typeof gtmId !== 'string' || !gtmId.length)) {
                return;
            } else if (typeof gtmId !== 'string' || !gtmId.length) {
                console.error('Google Tag Manager.', 'Missing/wrong `gtmId`.');
            } else {
                return `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
            }
        }

        try {
            const dataLayer = [requiredDataPoint].concat(customDataPoints);
            /* Get/set global dataLayer on global Object (e.g., `window`).
            * Custom data points should be set before GTM script loads. */
            globalObject['dataLayer'] = globalObject['dataLayer'] ?
                [...globalObject['dataLayer'], ...dataLayer]
                : dataLayer;
        } catch (error) {
            if (!dev) console.error('Google Tag Manager.', error);
        } finally {
            return getScriptSrcForGtm; // …no matter what, for no error.
        }
    }

    onMount( () => {
        if (!timeout) scriptSrc = getScriptSrcFromInitGtm( gtmDataPoints )( gtmId );
        else setTimeout(() => {
            scriptSrc = getScriptSrcFromInitGtm( gtmDataPoints )( gtmId );
        }, timeout);
    });
</script>

<svelte:head>
    {#if scriptSrc}
        <script src="{scriptSrc}" defer></script>
    {/if}
</svelte:head>