Why We Removed Environment Guards from Analytics Script Injection
The Problem with Environment-Based Analytics Loading
A few months ago, our AustinsElite app—a Laravel 12–powered platform with a modern frontend—loaded Google Analytics 4 only in production. The logic seemed sound at first: skip tracking in development and staging to avoid polluting analytics with non-user data. We wrapped the gtag.js script injection behind an environment check, a pattern we’d seen in tutorials and legacy docs.
But reality had other plans.
We started noticing gaps in event validation. QA couldn’t confirm if tracking was firing correctly on staging. Product managers wanted to verify funnel behavior before launch, but we had no visibility. Every analytics issue became a production-only mystery. We were flying blind until users hit the live site.
The environment guard, meant to protect data integrity, was actually hurting our ability to ship confidently.
Why Conditional Injection Backfires
Here’s the core issue: just because you’re not in production doesn’t mean you shouldn’t track. You should track—just not into your primary production property.
Our original code looked like this:
if (process.env.NODE_ENV === 'production') {
// Inject GA script
}
Simple. Clean. And totally broken for real-world testing.
Staging environments are where you want to validate events, test conversions, and catch broken parameters. By disabling analytics there, we created a blind spot. We’d deploy what we thought was working code, only to discover missing events or malformed payloads days later—after real users were affected.
Worse, developers started adding console.log('GA event:', ...) as a crutch. That’s not observability—that’s duct tape.
We realized we weren’t protecting data; we were sacrificing debuggability for a false sense of cleanliness.
A Better Approach: Config-Driven, Not Environment-Gated
So we flipped the model. Instead of blocking analytics by environment, we now load the GA4 script unconditionally—but route it based on configuration.
We introduced a NEXT_PUBLIC_GA_MEASUREMENT_ID environment variable across all environments, but with different values:
- Local:
G-DEV-XXXXXXX - Staging:
G-STG-XXXXXXX - Production:
G-PROD-XXXXXXX
The script loads the same way everywhere:
// _app.js or custom <Script> component
if (gaMeasurementId) {
// Inject gtag.js with gaMeasurementId
}
No process.env checks. No conditional rendering of tracking scripts. Just consistent injection, powered by config.
Events fire the same way in every environment. The only difference is where they land.
We also added a simple opt-out mechanism using window['ga-disable-' + measurementId] for users who want to block tracking—something we couldn’t easily test before because GA wasn’t loading at all outside production.
The Benefits We Didn’t Expect
The fix seemed small, but the impact was outsized.
First, QA got immediate feedback. When a button click was supposed to fire a purchase_initiated event, we could open browser dev tools, watch the Network tab, and confirm it fired—on staging. No more guessing.
Second, developers started writing better event code. Knowing their tracking would be tested early, they paid more attention to parameter naming, consistency, and edge cases.
Third, we caught a broken user_id pass-through during a staging review—something that would’ve leaked PII if not caught. Because GA was active, we saw the malformed payload in debug view immediately.
Finally, rollbacks became safer. If we deploy a new event structure and it causes issues, we can compare staging behavior before and after—because the tracking setup is identical.
Key Takeaway: Track Everywhere, Route Wisely
Don’t gate analytics on environment. That pattern belongs in 2015.
Instead, embrace full visibility across your stack—with smart routing. Use separate measurement IDs. Leverage GA4’s debug mode. Let your team test tracking like any other feature.
At AustinsElite, this shift didn’t just fix blind spots—it changed how we think about observability. Tracking isn’t a production-only concern. It’s part of the feature.
Now, when we ask "Did that event fire?", we don’t need to wait for production. We check staging. And we know—immediately.