Skip to content

Vercel Web Analytics served its script from a path with no leading slash

Posted in Vercel, Analytics, Debugging

By Dušan Dželebdžić

Photo by Matt Ashby on Unsplash
Photo by Matt Ashby on Unsplash

I turned on Vercel Web Analytics for this site the other day. It's a static Eleventy build, Speed Insights was already running from a one-line script tag, and I expected analytics to be the same one line. It nearly was.

There's no HTML option

The "Get Started" panel in the Vercel dashboard has a framework dropdown: Next.js, Remix, React, Sveltekit, Nuxt, Astro, Vue.js, Other. The docs mention an HTML variant with a plain script tag, but the dropdown doesn't offer it, and "Other" wants you to npm i @vercel/analytics and call inject() from your bundle.

I didn't want a React-flavoured npm package in a site that has one JS file for a mobile menu. The docs for the HTML variant show this:

<script>
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
</script>
<script defer src="/<unique-path>/script.js"></script>

That <unique-path> is the interesting part. The classic route is /_vercel/insights/script.js, which every ad blocker list has learned by now. The unique path is a per-project hash that the blockers haven't, so it's worth having. The dashboard just wasn't going to tell me what it was.

Where the unique path comes from

Further down the package docs, under "Dynamic configuration", there's this:

In version 2, Vercel passes default client options at build time with a JSON string under an analytics key

The env var is VERCEL_OBSERVABILITY_CLIENT_CONFIG, and the documented shape is:

{
"analytics": {
"scriptSrc": "/<unique-path>/script.js",
"eventEndpoint": "/<unique-path>/event",
"viewEndpoint": "/<unique-path>/view"
}
}

The npm package reads that during the build. Nothing stops a static site generator from reading it too. So I gave Eleventy a shortcode that parses the variable and prints the script tag, with the classic route as the fallback for local builds:

eleventyConfig.addShortcode("webAnalytics", () => {
let scriptSrc = "/_vercel/insights/script.js"
try {
const config = JSON.parse(process.env.VERCEL_OBSERVABILITY_CLIENT_CONFIG || "{}")
if (config.analytics && config.analytics.scriptSrc) scriptSrc = config.analytics.scriptSrc
} catch (e) {
console.warn("VERCEL_OBSERVABILITY_CLIENT_CONFIG is not valid JSON")
}
return `<script>window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };</script>
<script defer src="
${scriptSrc}"></script>`

})

I pushed it and looked at the source of the homepage. A script tag with a random hash in it, exactly as ordered.

The tag that looked wrong

Then I looked at the source of a blog post:

<script defer="defer" src="64af96b86432b6ec/script.js"></script>

No leading slash. The documented value starts with /. The real one doesn't.

On the homepage that doesn't matter. A relative 64af96b86432b6ec/script.js on / resolves to /64af96b86432b6ec/script.js, which is right. On /blog/docker-stop-all-containers/ it resolves to /blog/docker-stop-all-containers/64af96b86432b6ec/script.js, which is a 404 page rendered by Eleventy.

$ curl -s -o /dev/null -w '%{http_code}\n' https://ddz.dev/64af96b86432b6ec/script.js
200
$ curl -s -o /dev/null -w '%{http_code}\n' https://ddz.dev/blog/docker-stop-all-containers/64af96b86432b6ec/script.js
404

So analytics was live, and it was counting exactly one page. Every post on the site, which is where all the traffic goes, was loading a 404 instead of the script and reporting nothing. The dashboard doesn't flag this. It shows you a nice chart of the one URL it knows about.

The npm package presumably handles this somewhere in its own code, which is why nobody using the React component would ever notice. Read the variable yourself and you get the raw value.

The fix

One regex. Prefix a slash unless the value is already rooted or a full URL:

scriptSrc = config.analytics.scriptSrc.replace(/^(?!https?:\/\/|\/)/, "/")

After the next deploy the post pages had src="/64af96b86432b6ec/script.js" and the blog paths started showing up in the dashboard.

If you'd rather not bother

/_vercel/insights/script.js still returns 200 on a project with analytics enabled, and it works from any page because it's rooted. You lose the ad-blocker resistance of the unique path, and for a developer blog that's a real chunk of the audience. But it's one line, it can't 404, and it's what Speed Insights does anyway.

Takeaway

If a script tag in your source has a path that starts with a letter instead of a slash, it's not a path. It's a different path on every page.

Found this useful? Pass it on.

Follow me on X or LinkedIn for the next one.