Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
JavaScript to send Web Vitals to Google Analytics with debug information
// NOTE set up a new dimension in Google Analytics and then add the dimension number on line 91
// Based on Phil Walton's post: https://web.dev/debug-web-vitals-in-the-field/
<script type="module">
// Load the web-vitals library from unpkg.com (or host locally):
import {getFCP, getLCP, getCLS, getTTFB, getFID} from 'https://unpkg.com/web-vitals?module';
function getSelector(node, maxLen = 100) {
let sel = '';
try {
while (node && node.nodeType !== 9) {
const part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
(node.className && node.className.length) ?
'.' + Array.from(node.classList.values()).join('.') : '');
if (sel.length + part.length > maxLen - 1) return sel || part;
sel = sel ? part + '>' + sel : part;
if (node.id) break;
node = node.parentNode;
}
} catch (err) {
// Do nothing...
}
return sel;
}
function getLargestLayoutShiftEntry(entries) {
return entries.reduce((a, b) => a && a.value > b.value ? a : b);
}
function getLargestLayoutShiftSource(sources) {
return sources.reduce((a, b) => {
return a.node && a.previousRect.width * a.previousRect.height >
b.previousRect.width * b.previousRect.height ? a : b;
});
}
function wasFIDBeforeDCL(fidEntry) {
const navEntry = performance.getEntriesByType('navigation')[0];
return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
}
function sendWebVitals() {
function sendWebVitalsGAEvents({name, delta, id, entries}) {
if ("function" == typeof ga) {
let webVitalInfo = '(not set)';
// Set a custom dimension for more info for any CVW breaches
// In some cases there won't be any entries (e.g. if CLS is 0,
// or for LCP after a bfcache restore), so we have to check first.
if (entries.length) {
if (name === 'LCP') {
const lastEntry = entries[entries.length - 1];
webVitalInfo = getSelector(lastEntry.element);
} else if (name === 'FID') {
const firstEntry = entries[0];
webVitalInfo = getSelector(firstEntry.target);
} else if (name === 'CLS') {
const largestEntry = getLargestLayoutShiftEntry(entries);
if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
if (largestSource) {
webVitalInfo = getSelector(largestSource.node);
}
}
}
}
ga('send', 'event', {
eventCategory: 'Web Vitals',
eventAction: name,
// The `id` value will be unique to the current page load. When sending
// multiple values from the same page (e.g. for CLS), Google Analytics can
// compute a total by grouping on this ID (note: requires `eventLabel` to
// be a dimension in your report).
eventLabel: id,
// Google Analytics metrics must be integers, so the value is rounded.
// For CLS the value is first multiplied by 1000 for greater precision
// (note: increase the multiplier for greater precision if needed).
eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
// Use a non-interaction event to avoid affecting bounce rate.
nonInteraction: true,
// Use `sendBeacon()` if the browser supports it.
transport: 'beacon',
// OPTIONAL: any additional params or debug info here.
// See: https://web.dev/debug-web-vitals-in-the-field/
// dimension1: '...',
// dimension2: '...',
dimension1: webVitalInfo
// ...
});
}
}
// Register function to send Core Web Vitals and other metrics as they become available
getFCP(sendWebVitalsGAEvents);
getLCP(sendWebVitalsGAEvents);
getCLS(sendWebVitalsGAEvents);
getTTFB(sendWebVitalsGAEvents);
getFID(sendWebVitalsGAEvents);
}
sendWebVitals();
</script>
@Kishorchandth

This comment has been minimized.

Copy link

@Kishorchandth Kishorchandth commented May 17, 2021

Please rectify the code.
There are some issues in the code and I have wasted 2 days of data collection and not able to see my debug information.

<script type="module">
  // Load the web-vitals library from unpkg.com (or host locally):
  //import {getFCP, getLCP, getCLS, getTTFB, getFID} from 'https://unpkg.com/web-vitals?module''; - wrong double apostrophe

  import {getFCP, getLCP, getCLS, getTTFB, getFID} from 'https://unpkg.com/web-vitals?module';
  function getSelector(node, maxLen = 100) {
    let sel = '';
    try {
      while (node && node.nodeType !== 9) {
        const part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
          (node.className && node.className.length) ?
          '.' + Array.from(node.classList.values()).join('.') : '');
        if (sel.length + part.length > maxLen - 1) return sel || part;
        sel = sel ? part + '>' + sel : part;
        if (node.id) break;
        node = node.parentNode;
      }
    } catch (err) {
      // Do nothing...
    }
    return sel;
  }
  
  function getLargestLayoutShiftEntry(entries) {
    return entries.reduce((a, b) => a && a.value > b.value ? a : b);
  }
  
  function getLargestLayoutShiftSource(sources) {
    return sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    });
  }
  
  function wasFIDBeforeDCL(fidEntry) {
    const navEntry = performance.getEntriesByType('navigation')[0];
    return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
  }
  
  function sendWebVitals() {
    function sendWebVitalsGAEvents({name, delta, id, entries}) {
      if ("function" == typeof ga) {
  
        let webVitalInfo = '(not set)';
  
        // Set a custom dimension for more info for any CVW breaches
        if (name === 'LCP') {
           const lastEntry = entries[entries.length - 1];
           webVitalInfo =  getSelector(lastEntry.element);
        } else if (name === 'FID') {
          const firstEntry = entries[0];
          webVitalInfo = getSelector(firstEntry.target);
        } else if (name === 'CLS') {
           const largestEntry = getLargestLayoutShiftEntry(entries);
           if (largestEntry && largestEntry.sources) {
              const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
              if (largestSource) {
                webVitalInfo = getSelector(largestSource.node);
              }
           }
        }
  
        ga('send', 'event', {
          eventCategory: 'Web Vitals',
          eventAction: name,
          // The `id` value will be unique to the current page load. When sending
          // multiple values from the same page (e.g. for CLS), Google Analytics can
          // compute a total by grouping on this ID (note: requires `eventLabel` to
          // be a dimension in your report).
          eventLabel: id,
          // Google Analytics metrics must be integers, so the value is rounded.
          // For CLS the value is first multiplied by 1000 for greater precision
          // (note: increase the multiplier for greater precision if needed).
          eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
          // Use a non-interaction event to avoid affecting bounce rate.
          nonInteraction: true,
          // Use `sendBeacon()` if the browser supports it.
          transport: 'beacon',
  
          // OPTIONAL: any additional params or debug info here.
          // See: https://web.dev/debug-web-vitals-in-the-field/
          // dimension1: '...',
          // dimension2: '...',
          **//   dimension1: webVitalInfo this is wrong**
          dimension1: 'webVitalInfo',
          // ...
        });
      }
    }

    // Register function to send Core Web Vitals and other metrics as they become available
    getFCP(sendWebVitalsGAEvents);
    getLCP(sendWebVitalsGAEvents);
    getCLS(sendWebVitalsGAEvents);
    getTTFB(sendWebVitalsGAEvents);
    getFID(sendWebVitalsGAEvents);
  
  }
  
  sendWebVitals();
</script>
@tunetheweb

This comment has been minimized.

Copy link
Owner Author

@tunetheweb tunetheweb commented May 17, 2021

Is it just lines 3 and 83 you have corrected? Very difficult to see since you've pasted the whole script. I have fixed those - apologies, copy paste issues when adding to the gist.

@Kishorchandth

This comment has been minimized.

Copy link

@Kishorchandth Kishorchandth commented May 17, 2021

I can't confirm everything because my JS skill is pretty much zero.

I did a side-by-side comparison with Debug Web Vitals in the field & Chrome Dev tools - console tab for line 3 and using Custom Dimensions and Metrics for dimension.

Thank you for the Code.

@Kishorchandth

This comment has been minimized.

Copy link

@Kishorchandth Kishorchandth commented May 18, 2021

Sorry for Disturbing you again.
I set up Custom Dimension in my Google Analytics accounts.

Is this Correct? or do I need to change it


<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};	
	var dimensionValue = 'webVitalInfo';
	ga('create', 'UA-54330676-12', 'auto');
	ga("set", "transport", "beacon"),
        ga("set", "anonymizeIp", !0),
        ga("set", "allowAdFeatures", !1),		
        setTimeout("ga( 'send' , 'event' , 'Adjusted Bounce Rate',  '6 seconds')", 8e3),
   	ga('send', 'pageview', {
  'dimension1':  'dimensionValue'
});
</script>

<script async defer src="https://www.google-analytics.com/analytics_debug.js" importance="low"></script>
<script type="module">
  // Load the web-vitals library from unpkg.com (or host locally):
  import {getFCP, getLCP, getCLS, getTTFB, getFID} from 'https://unpkg.com/web-vitals?module';
  
  function getSelector(node, maxLen = 100) {
    let sel = '';
    try {
      while (node && node.nodeType !== 9) {
        const part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
          (node.className && node.className.length) ?
          '.' + Array.from(node.classList.values()).join('.') : '');
        if (sel.length + part.length > maxLen - 1) return sel || part;
        sel = sel ? part + '>' + sel : part;
        if (node.id) break;
        node = node.parentNode;
      }
    } catch (err) {
      // Do nothing...
    }
    return sel;
  }
  
  function getLargestLayoutShiftEntry(entries) {
    return entries.reduce((a, b) => a && a.value > b.value ? a : b);
  }
  
  function getLargestLayoutShiftSource(sources) {
    return sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    });
  }
  
  function wasFIDBeforeDCL(fidEntry) {
    const navEntry = performance.getEntriesByType('navigation')[0];
    return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
  }
  
  function sendWebVitals() {
    function sendWebVitalsGAEvents({name, delta, id, entries}) {
      if ("function" == typeof ga) {
  
        let webVitalInfo = '(not set)';
  
        // Set a custom dimension for more info for any CVW breaches
        if (name === 'LCP') {
           const lastEntry = entries[entries.length - 1];
           webVitalInfo =  getSelector(lastEntry.element);
        } else if (name === 'FID') {
          const firstEntry = entries[0];
          webVitalInfo = getSelector(firstEntry.target);
        } else if (name === 'CLS') {
           const largestEntry = getLargestLayoutShiftEntry(entries);
           if (largestEntry && largestEntry.sources) {
              const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
              if (largestSource) {
                webVitalInfo = getSelector(largestSource.node);
              }
           }
        }
  
        ga('send', 'event', {
          eventCategory: 'Web Vitals JS',
          eventAction: name,
          // The `id` value will be unique to the current page load. When sending
          // multiple values from the same page (e.g. for CLS), Google Analytics can
          // compute a total by grouping on this ID (note: requires `eventLabel` to
          // be a dimension in your report).
          eventLabel: id,
          // Google Analytics metrics must be integers, so the value is rounded.
          // For CLS the value is first multiplied by 1000 for greater precision
          // (note: increase the multiplier for greater precision if needed).
          eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
          // Use a non-interaction event to avoid affecting bounce rate.
          nonInteraction: true,
          // Use `sendBeacon()` if the browser supports it.
          transport: 'beacon',
  
          // OPTIONAL: any additional params or debug info here.
          // See: https://web.dev/debug-web-vitals-in-the-field/
          // dimension1: '...',
          // dimension2: '...',
          dimension1: 'webVitalInfo'
          // ...
        });
      }
    }

    // Register function to send Core Web Vitals and other metrics as they become available
    getFCP(sendWebVitalsGAEvents);
    getLCP(sendWebVitalsGAEvents);
    getCLS(sendWebVitalsGAEvents);
    getTTFB(sendWebVitalsGAEvents);
    getFID(sendWebVitalsGAEvents);
  
  }
  
  sendWebVitals();
</script>

@tunetheweb

This comment has been minimized.

Copy link
Owner Author

@tunetheweb tunetheweb commented May 18, 2021

Is this your first custom dimension? If so dimension1 is the correct one to use. If you already have other custom dimensions then you may have to change that to dimensionX (where X is the number you use).

This bit is not correct:

<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};	
	var dimensionValue = 'webVitalInfo';
	ga('create', 'UA-54330676-12', 'auto');
	ga("set", "transport", "beacon"),
        ga("set", "anonymizeIp", !0),
        ga("set", "allowAdFeatures", !1),		
        setTimeout("ga( 'send' , 'event' , 'Adjusted Bounce Rate',  '6 seconds')", 8e3),
   	ga('send', 'pageview', {
  'dimension1':  'dimensionValue'
});
</script>

That looks like the standard GA snippet to set up GA and also log your page views and some Adjusted Bounce Rate event. So remove the two lines a live dimensionValue - they are not needed here.

The sendWebVitals() function at the end will log event handlers to listen for your Core Web Vitals and then send them as events to GA when they appear. So they do not need to be sent at the beginning during set up.

Happy to check out a site to make sure it’s correct after you fix that. Let me know the URL or DM me on Twitter (@tunetheweb) if not comfortable sharing publicly.

@Kishorchandth

This comment has been minimized.

Copy link

@Kishorchandth Kishorchandth commented May 18, 2021

I set up like this


<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
       
      
	var customDimension = 'webVitalInfo';
	  ga("set", "transport", "beacon"),
         ga("set", "anonymizeIp", !0),    
         // adjusted Bounce rate
         setTimeout("ga( 'send' , 'event' , 'Adjusted Bounce Rate',  '6 seconds')", 8e3),
	ga('create', 'UA-54330676-13', 'auto');
        // Seems like I have 2 Custom Dimension
	ga('set', 'dimension2', customDimension );
	ga('send', 'pageview');

</script>

and rest of the Code is yours and I have set up dimension2: 'webVitalInfo'. I think the Custom Dimension is correct.

You can see the code on my blog too

@tunetheweb

This comment has been minimized.

Copy link
Owner Author

@tunetheweb tunetheweb commented May 18, 2021

When I use the Google Analytics Chrome Extension I see this:

Console Screenshot

This tells me a few things:

  1. You are sending the dimension2 in your initial Google Analytics set up. As I said before you do NOT need to do this. The library will send events as they happen as you can see in the events below. Remove the var customDimension = 'webVitalInfo'; and ga('set', 'dimension2', customDimension ); lines from your snippet above - you don't need them and shouldn't be calling this.
  2. You are sending the string 'webVitalInfo' instead of the value of the webVitalInfo variable. Change line 184 from dimension2: 'webVitalInfo', to dimension2: webVitalInfo (i.e,. remove the quotes, and you can also remove the comma since it's the last item in the list)
  3. You have no CLS at all - well done you! But that does show a problem in the code so have fixed that by adding , null to all the reduce calls (lines 26 and 33 of my original gist), which should avoid that error at the end. yu should make a similar change to your code.
@Kishorchandth

This comment has been minimized.

Copy link

@Kishorchandth Kishorchandth commented May 19, 2021

Thank you so much again really appreciate it, It is working https://prnt.sc/133xmay

@tunetheweb

This comment has been minimized.

Copy link
Owner Author

@tunetheweb tunetheweb commented May 19, 2021

Actually I discovered a bug while testing this.

You should change this:

  function getLargestLayoutShiftSource(sources) {
    return sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    }, null);
  }

to this:

  function getLargestLayoutShiftSource(sources) {
    return sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    });
  }

i.e. remove the last , null I made you add in error.

Won't affect you at the moment as you have such perfect CLS scores but still, better to correct in case that ever changes.

@tunetheweb

This comment has been minimized.

Copy link
Owner Author

@tunetheweb tunetheweb commented May 19, 2021

I've updated the Gist again. Turns out I was missing an if statement which I've just added on line 52 (and the close on line 68). With that in place you don't need any of the ,null code that I previously advised adding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment