Skip to content

·  · 6 min read

Your Google Analytics Is Silently Dead: The Partytown Trap in Astro Themes

My Google Analytics was installed, verified, and collecting absolutely nothing — with zero errors to warn me. The culprit was a Partytown default in my Astro theme that emits GA as a script the browser never runs. Here's the trap, and the one-line fix.

My Google Analytics was installed, verified, and collecting absolutely nothing — with zero errors to warn me. The culprit was a Partytown default in my Astro theme that emits GA as a script the browser never runs. Here's the trap, and the one-line fix.

Google Analytics was installed on my Astro site. The tag was verified. The site was live and getting traffic. And the GA4 dashboard was a perfectly flat line — zero sessions, zero events, no error anywhere. Nothing told me it was broken. That’s the worst kind of bug: the silent one. The cause was a Partytown default buried in my theme, and if you run a portfolio or blog on an AstroWind-style theme, there’s a real chance yours is dead too and you don’t know it.

Here’s the whole trap, why it happens at the code level, and the one-line fix.

The 30-second test: is your GA actually firing?

Before touching any code, confirm the symptom. Open your live site, open DevTools → Network, filter for collect, and reload:

  • Working GA: you’ll see a request to google-analytics.com/g/collect (or region1.google-analytics.com/g/collect) returning 204.
  • Dead GA: nothing. No collect request at all.

Then look at the page source (Elements tab) for the gtag script. If GA is dead the way mine was, you’ll find something like this:

<script type="text/partytown" async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>

See that type="text/partytown"? That’s the smoking gun. The browser does not recognize text/partytown as executable JavaScript, so it parses the tag, shrugs, and never runs it. Your analytics script is sitting right there in the HTML, inert. GA “is installed” and collects nothing. (If you’re still assembling the rest of your setup, analytics is one piece of a larger checklist I cover in the Astro SEO setup most portfolios skip.)

Why it happens: a default that assumes an integration you turned off

Partytown is a genuinely good idea — it moves heavy third-party scripts (like GA) off the main thread into a web worker, so they don’t hurt your Core Web Vitals. The type="text/partytown" attribute is how you tell Partytown “take this script and run it in the worker instead of the main thread.”

The catch: type="text/partytown" only does something if the Partytown library is actually loaded on the page to pick those scripts up. If it isn’t, that attribute is just a string the browser ignores.

And that’s exactly the contradiction AstroWind-based themes ship with. Three pieces line up to kill GA:

1. The theme defaults partytown to true. In the config builder, the Google Analytics integration is configured to use Partytown out of the box:

// vendor/integration/utils/configBuilder.ts
partytown: true,

2. So the GA component tags its scripts for Partytown. The @astrolib/analytics <GoogleAnalytics> component reads that prop and, when it’s truthy, stamps the text/partytown type onto both the gtag loader and the inline config script:

---
// @astrolib/analytics/GoogleAnalytics.astro
const { id, partytown = false } = Astro.props;
const attrs = partytown ? { type: 'text/partytown' } : {};
---

<script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${id}`} {...attrs}></script>
<script is:inline define:vars={{ id }} {...attrs}>
  // ...gtag('config', id)
</script>

3. But the Partytown integration is disabled. In astro.config.mjs, the theme gates Partytown behind a flag — and that flag ships as false:

// astro.config.mjs
const hasExternalScripts = false;
const whenExternalScripts = (items = []) =>
  hasExternalScripts ? (Array.isArray(items) ? items.map((i) => i()) : [items()]) : [];

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

With hasExternalScripts = false, whenExternalScripts(...) returns an empty array, so the Partytown integration is never registered. No Partytown runtime ships to the browser.

Put the three together and you get the trap: the theme tags GA for Partytown while never loading Partytown to handle it. The browser sees type="text/partytown", decides that’s not JavaScript it knows how to run, and skips it. GA is emitted and never executes. No error, because nothing failed — the browser did exactly what an unknown script type tells it to do: nothing.

The fix (one line)

You have two correct fixes, depending on whether you want GA on the main thread or off it.

Option A — make GA a normal script (one line, ships instantly). Tell the GA integration to stop tagging for Partytown. In config.yaml:

analytics:
  vendors:
    googleAnalytics:
      id: 'G-XXXXXXXXXX'
      partytown: false # load GA as a normal async <script> on the main thread

Now attrs is empty, the text/partytown type disappears, and the browser runs gtag like any other script. This is the fix I shipped — for a static blog, GA’s main-thread cost is negligible and the reliability is worth it.

Option B — actually turn Partytown on. If you care about squeezing GA off the main thread for Core Web Vitals, flip the other side of the contradiction: set hasExternalScripts = true in astro.config.mjs. Partytown is already a dependency in these themes, so this registers the runtime that makes text/partytown mean something. Slightly better CWV, slightly more moving parts.

Pick one. The bug is having neither consistent: tagged for Partytown, but no Partytown. Don’t do both half-way.

Verify it’s alive

Deploy the fix, then repeat the 30-second test: reload the live page with the Network tab open, filtered for collect. You should now see the g/collect request fire with a 204. Cross-check GA4 → Reports → Realtime and load your own site — you should show up as an active user within seconds.

The real lesson: watch for defaults that assume an integration you disabled

This isn’t really a Partytown bug or an Astro bug. Partytown does exactly what it’s told; Astro renders exactly what the component emits. The bug lives in the seam — a default (partytown: true) that silently assumes a capability (hasExternalScripts: true) that a different config file turned off. Two files, each locally reasonable, globally contradictory.

Those are the failures that cost you months, because nothing throws. The habit worth building: when you wire up analytics, a payment webhook, an email send — anything where “no error” and “not working” look identical — don’t trust the dashboard, verify the wire. One look at the Network tab would have told me in week one what a flat GA chart took much longer to make me suspect. It’s the same discipline I fold into my routine for validating an Astro blog post before it goes live — check the rendered output, not just that the build passed.

If you’re setting up one of these themes from scratch, my deep-dive on the AstroWind template walks through how its config layers fit together — the same layering that made this trap possible. And if you want the tooling I use to catch exactly this class of “silent seam” bug faster, that’s in my post on the AI dev stack I build with.

Go check your own collect request right now. It takes 30 seconds, and the flat-line version of this story is a lot more expensive.

    Share:

    Enjoyed this post?

    Get new posts on web dev, AI and SEO straight to your inbox. No spam, unsubscribe anytime.

    No spam. By subscribing you agree to the privacy policy .

    Back to blog

    Related Posts

    View All Posts »
    How I Cut My Homepage Image Weight 88% (Astro + Sharp + WebP)

    How I Cut My Homepage Image Weight 88% (Astro + Sharp + WebP)

    A static Astro site with 15KB of JavaScript was shipping 3.4MB of images. Here are the three failure modes that got past the image pipeline — the fallback-always-ships rule, the screenshot the optimizer never touched, and the boilerplate nobody referenced — and the 15 lines of Sharp that fixed all of it.