/ astro-integrations / How to Integrate Google Analytics with Astro: Complete Guide
astro-integrations 5 min read

How to Integrate Google Analytics with Astro: Complete Guide

Step-by-step guide to integrating Google Analytics with your Astro website. Setup, configuration, and best practices.

How to Integrate Google Analytics with Astro: Complete Guide

Google Analytics 4 (GA4) is the standard web analytics platform for tracking user behavior, traffic sources, conversions, and engagement metrics. Adding GA4 to your Astro site lets you understand how visitors interact with your content, which pages perform best, and where your traffic comes from. Since Astro outputs static HTML by default, the integration involves adding the GA4 tracking script to your pages and optionally configuring it for more advanced tracking scenarios.

This guide covers adding GA4 to an Astro project, handling view transitions, setting up custom events, and respecting user privacy preferences.

Prerequisites

You will need:

  • Node.js 18+ installed
  • An existing Astro project (any version)
  • A Google Analytics 4 property created at analytics.google.com
  • Your GA4 Measurement ID (starts with G-, found in Admin > Data Streams > Web)

No SSR adapter is required. Google Analytics works with static Astro sites.

Installation

There is no npm package to install. GA4 uses a script tag loaded directly from Google's servers. However, if you want a community integration with extra features:

npx astro add @astrojs/partytown

Partytown is optional but recommended. It offloads the analytics script to a web worker, preventing it from blocking your page rendering. This improves Core Web Vitals scores.

Configuration

Basic Setup Without Partytown

The simplest approach is adding the GA4 script directly to your layout component:

---
// src/layouts/BaseLayout.astro
const GA_ID = 'G-XXXXXXXXXX'; // Replace with your Measurement ID
---

<html>
  <head>
    <script async src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}></script>
    <script define:vars={{ GA_ID }}>
      window.dataLayer = window.dataLayer || [];
      function gtag() { dataLayer.push(arguments); }
      gtag('js', new Date());
      gtag('config', GA_ID);
    </script>
  </head>
  <body>
    <slot />
  </body>
</html>

The define:vars directive passes the Astro variable into the inline script without breaking the build.

Setup With Partytown

For better performance, use Partytown to run the analytics script off the main thread:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import partytown from '@astrojs/partytown';

export default defineConfig({
  integrations: [
    partytown({
      config: {
        forward: ['dataLayer.push'],
      },
    }),
  ],
});

Then update your layout to use the type="text/partytown" attribute:

---
// src/layouts/BaseLayout.astro
const GA_ID = 'G-XXXXXXXXXX';
---

<html>
  <head>
    <script
      type="text/partytown"
      async
      src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
    ></script>
    <script type="text/partytown" define:vars={{ GA_ID }}>
      window.dataLayer = window.dataLayer || [];
      function gtag() { dataLayer.push(arguments); }
      gtag('js', new Date());
      gtag('config', GA_ID);
    </script>
  </head>
  <body>
    <slot />
  </body>
</html>

Partytown intercepts the scripts and runs them in a web worker, keeping the main thread free for user interactions.

Using Environment Variables

Store the measurement ID in your .env file to keep it out of source code:

PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX

Then reference it in your layout:

---
const GA_ID = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
---

Common Patterns

Handling Astro View Transitions

If your Astro site uses View Transitions, page navigations happen without full page reloads. GA4 will not automatically track these. Add a listener for Astro's navigation event:

<script>
  document.addEventListener('astro:page-load', () => {
    if (typeof gtag === 'function') {
      gtag('event', 'page_view', {
        page_title: document.title,
        page_location: window.location.href,
      });
    }
  });
</script>

Place this script in your base layout so it runs on every page. The astro:page-load event fires on both initial load and subsequent client-side navigations.

Custom Event Tracking

Track specific user interactions by sending custom events:

<button id="signup-btn">Sign Up Free</button>

<script>
  document.getElementById('signup-btn')?.addEventListener('click', () => {
    gtag('event', 'sign_up_click', {
      event_category: 'engagement',
      event_label: 'hero_section',
    });
  });
</script>

To comply with GDPR and similar regulations, load GA4 only after the user gives consent:

<script define:vars={{ GA_ID }}>
  window.dataLayer = window.dataLayer || [];
  function gtag() { dataLayer.push(arguments); }

  // Default to denied
  gtag('consent', 'default', {
    analytics_storage: 'denied',
  });

  // Load gtag.js
  const script = document.createElement('script');
  script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
  script.async = true;
  document.head.appendChild(script);

  gtag('js', new Date());
  gtag('config', GA_ID);

  // Call this function when user accepts cookies
  window.grantAnalyticsConsent = function () {
    gtag('consent', 'update', {
      analytics_storage: 'granted',
    });
  };
</script>

This approach loads GA4 but prevents it from storing cookies until consent is granted. When the user clicks "Accept" in your cookie banner, call window.grantAnalyticsConsent().

Troubleshooting

No data appearing in GA4 dashboard: GA4 has a processing delay of up to 24-48 hours for standard reports. Use the Realtime report to verify the script is working. Also check that you are using the correct Measurement ID (starts with G-, not UA-).

Partytown script not executing: Partytown requires the forward config to include dataLayer.push. Without this, gtag calls silently fail. Also make sure your build output includes the Partytown library files in the ~partytown directory.

Duplicate page views: If you are using View Transitions and the standard GA4 setup, you might get double counting. Either use the astro:page-load approach with send_page_view: false in the config, or let GA4 handle it automatically without the custom listener.

GA4 blocked by ad blockers: Many users run ad blockers that prevent GA4 from loading. Consider a server-side analytics solution like Plausible or Umami as a supplement if you need more accurate numbers. GA4 data should be treated as a sample rather than an exact count.

Core Web Vitals impact: Without Partytown, the GA4 script adds roughly 28KB of JavaScript. If your Lighthouse scores drop after adding analytics, enable Partytown or load the script with a setTimeout to defer it after page interaction.

Conclusion

Google Analytics 4 integrates cleanly with Astro through a simple script tag in your base layout. Use Partytown for better performance, handle View Transitions with the astro:page-load event, and implement consent mode for privacy compliance. For most Astro sites, the environment variable approach with the basic script tag provides everything you need. Start with the Realtime report to verify your setup before diving into the full analytics dashboard.