Vercel Web Analytics on Nuxt 4: @vercel/analytics breaks nuxt typecheck

Installing @vercel/analytics broke nuxt typecheck on Nuxt 4: a stale vue-router@^4 peer un-hoisted vue-router. We replaced it with one script tag.

June 10, 2026

The file with the highest comment-to-code ratio in this repo is app/plugins/vercel-analytics.client.ts. Six lines of comment, eight lines of code. The comment is long because the code looks like something you would reject in review: a hand-rolled script tag where an official npm package exists. The comment is there so the next agent or human who opens the file does not "fix" it by reinstalling @vercel/analytics.

Here is the whole file, comment included:

// Vercel Web Analytics, injected as the plain insights script rather than
// via @vercel/analytics, whose Nuxt build declares a stale `vue-router@^4`
// peer that conflicts with Nuxt 4's vue-router 5 (it un-hoists vue-router
// and breaks `nuxt typecheck`). The bare script tracks pageviews and SPA
// navigations on its own; Vercel serves it only on its own infra, so we
// skip it on localhost to avoid a dev-time 404.
export default defineNuxtPlugin(() => {
 const host = window.location.hostname
 if (host === 'localhost' || host === '127.0.0.1') return

 useHead({
 script: [{ src: '/_vercel/insights/script.js', defer: true }],
 })
})

This post is the long version of that comment.

The peer dependency that didn't match anything

boiledplate.ai runs on Nuxt 4, and Nuxt 4 depends on vue-router 5. Our lockfile has exactly one copy: vue-router@5.1.0, hoisted to the top of node_modules, which is where Nuxt, our pages, and the TypeScript project all expect to resolve it from.

When we added Vercel Web Analytics, we did the documented thing first: npm install @vercel/analytics and wire up its Nuxt integration. The package's Nuxt build declares a peer dependency of vue-router@^4. That range predates Nuxt 4 and matches nothing in our tree.

A peer range that can't be satisfied does not produce a clean error. npm resolves it by restructuring the tree, and vue-router stopped being the single hoisted copy our TypeScript setup resolved against. With router types now resolving inconsistently, nuxt typecheck failed. The command that our conventions file treats as the definition of done ("verify with npm run typecheck before declaring work done") was broken by a package that never touches the router in any way we needed.

That is the nasty property of stale peer ranges: the package installs fine, the app may even run, and the breakage shows up in tooling output that points nowhere near the package that caused it. We knew the culprit only because the failure appeared in the same session as the install. We have hit this shape of bug before, where a page returned 200 on the server and 500 in the browser because of an ordering detail three layers down. The cause and the symptom live in different places, and the distance between them is the cost.

The question that decided it: what does this package do?

We had three ways out.

Option one: overrides in package.json, forcing the nested peer to vue-router 5. Works, but it is a permanent lie in the manifest that someone has to remember exists, and it has to survive every future npm install and every dependency audit an agent runs in this repo.

Option two: --legacy-peer-deps. Same idea with less precision, and now every contributor and every CI environment needs the flag.

Option three: ask what @vercel/analytics actually does before deciding it is worth an exception. The answer changed the decision. On Vercel, the analytics tracker is not in the npm package. It is a script the platform itself serves at /_vercel/insights/script.js, and that script already tracks pageviews and SPA navigations on its own. The npm package is framework glue around inserting that script tag, and the framework glue is precisely the part carrying the stale vue-router@^4 peer.

So the dependency was buying us the one part of the product we did not need, at the price of a broken typecheck. We uninstalled it and wrote the plugin above.

Why each line is the way it is

The plugin is client-only (the .client.ts suffix) because there is nothing to do during SSR; the script is a browser tracker.

useHead injects the tag the Nuxt way instead of document.createElement, so it participates in head management like every other tag in the app.

The localhost guard is not decoration. Vercel serves /_vercel/insights/script.js only on its own infrastructure. On a local dev server that path does not exist, so without the guard every page load logs a 404 in the console. Skipping localhost and 127.0.0.1 keeps dev output clean, and production needs no opposite check because the script is simply there on Vercel hostnames.

One constraint outside the code mattered too. This site's privacy policy promises no tracking or analytics cookies, which is the reason boiledplate.ai has no consent banner at all. Vercel Web Analytics is cookie-free, so the bare script keeps that promise exactly as well as the package did. Any analytics product that needed a cookie would have been a privacy-page rewrite plus consent gating, not a dependency swap.

When you should keep the package

This is not "never use vendor SDKs." The honest accounting: @vercel/analytics gives you a typed track() for custom events, a dev debug mode, and maintained framework bindings. If you are sending custom events from a React or Next.js app where the peer ranges are current, the package is the right call.

Our needs are pageviews and route changes, both of which the platform script handles with zero configuration. The day we want custom events, we will weigh five lines against the script's global interface versus re-importing a dependency that, as of when we hit this, un-hoists our router. Until then, the package's entire value to us was inserting one script tag, and we can insert one script tag.

The habit worth taking away

When nuxt typecheck (or any type resolution) breaks right after an install, interrogate the tree before the code: npm ls vue-router, or whichever core package the errors orbit. A duplicated framework-level package is the usual answer, and the dependency that smuggled it in will not be named anywhere in the error output.

And when the fix is a script tag where a package used to be, write the comment. Eight lines of unusual code with no explanation is a refactor target; eight lines with six lines of "here is the bug this avoids" is a decision that survives the next contributor. In a repo built for AI coding agents, that comment is the difference between the fix sticking and an eager agent helpfully reinstalling the problem.

#nuxt #vercel #analytics #vue-router #peer-dependencies

Read more