TLDR: Vercel double-escapes \n in JSON env vars. One .replace() call and a try/catch fixes it. But the real lesson is the hardcoded fallbacks that were hiding the problem from me the whole time.

What I Was Building

I built a live performance dashboard for a cancer education client — total users, form submissions, organic traffic, deploy count, all pulling from GA4 (Google Analytics 4) and Netlify in real time.

Clean client-facing thing. Shareable link, token-based auth, monthly navigation.

Worked perfectly in development. Pushed it to Vercel, and the analytics panel just… died.

The Setup (and the Trap I'd Already Set for Myself)

The code had two modes for loading the GA4 service account credentials:

  • File path modeGOOGLE_SERVICE_ACCOUNT_KEY_PATH points to a JSON file on disk (great for local dev)
  • Inline modeGOOGLE_SERVICE_ACCOUNT_KEY holds the full JSON string as an env var (what you'd use on Vercel)

Locally I was running off the file. On Vercel, I pasted the entire service account JSON into the env var field in the dashboard and called it a day.

The original code was this:

credentials = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY || "{}");

Spot the trap? That || "{}" fallback. If the env var wasn't set — or was wrong — the code would happily parse an empty object and move on. No error. No crash. Just silently wrong.

I'd baked in a failure mode that looked like success.

What I Tried First (Wrong)

The error on Vercel was a cryptic Google auth failure. No useful message, no line number.

My first instinct: the \n characters in the private key are getting escaped.

When you paste a service account JSON into a Vercel env var, Vercel stores it as a plain string. The private_key field inside that JSON has literal newlines — and those can come out the other end as \\n (two characters: backslash + n) instead of real newline characters.

So I added a replace before the parse:

credentials = JSON.parse(raw.replace(/\\n/g, "\n"));

Deployed. Still broken.

The problem? I was replacing before parsing. If the JSON happened to parse fine but the key itself was malformed, this did nothing useful. And I still had no visibility into what Vercel was actually injecting at runtime.

The Fix That Worked

Two things, together.

First: I added diagnostic output directly to the error response — no secrets, just enough to see through Vercel's black box:

debug: {
  hasKeyEnv: raw.length > 0,
  keyEnvLength: raw.length,
  jsonParseOk: parseOk,
  clientEmail,   // from parsed JSON, or "missing"
}

Hit the endpoint. Saw jsonParseOk: false. The JSON itself wasn't parsing — Vercel's double-escaping was mangling the structure, not just the key.

Second: I flipped the parse logic to try-then-fix:

try {
  credentials = JSON.parse(raw);
} catch {
  // Vercel may double-escape newlines in the private key
  credentials = JSON.parse(raw.replace(/\\n/g, "\n"));
}

Try the clean parse first. If it throws, then strip the escaped newlines and try again.

That worked.

The Part That Stings

After the GA4 fix landed, I did one more commit: remove all the hardcoded fallbacks.

// Before
const raw = process.env.GOOGLE_SERVICE_ACCOUNT_KEY || "{}";
return process.env.GA4_PROPERTY_ID || "<GA4_PROPERTY_ID>";
return process.env.NETLIFY_SITE_ID || "<NETLIFY_SITE_ID>";

// After — each one now throws if the var is missing
if (!raw) throw new Error("GOOGLE_SERVICE_ACCOUNT_KEY is required");

Those fallbacks had been there since the first commit. And they were the reason I couldn't catch this sooner — locally, everything "worked," because the file path mode was active and nothing was actually testing the inline path.

The hardcoded || "{}" wasn't a safety net. It was a blindfold.

Why This Matters to Me

I've shipped enough Vercel projects to know that adding an env var does NOT update the running deployment — you have to redeploy. That one gets people constantly.

But this one was subtler. The env var was set. The deploy was fresh. The code was reaching the var. It just couldn't parse what came out.

The lesson I'm carrying forward: when you're debugging an env var problem on a platform you can't inspect directly, put diagnostic info in the error response itself — length, parse result, a non-secret field from the parsed value. Not logs. Not console output you have to chase through vercel logs. The response, right there, when you hit the failing endpoint.

And kill the fallbacks. They feel like good defensive coding. They're actually just silent failures waiting to happen.