analytics · nextjs · typescript · vercel
The analytics call that couldn't fail
Short answer: if your analytics events never show up and nothing ever errors, check whether the call site is written as window.someProvider?.(event). Optional chaining on a global that was never defined doesn't throw. It returns undefined and moves on. If no provider script is loaded, every event is a silent no-op, and a green build tells you nothing.
That was this site for nearly ten weeks.
What happened
The in-post CTAs on abdur.ai shipped in the first commit, 82c5c59, on June 28. Each one called a small helper on click:
function track(name: string) {
if (typeof window !== "undefined") {
// @ts-expect-error -- plausible global
window.plausible?.(name);
// @ts-expect-error -- vercel analytics
window.va?.("event", { name });
}
}
It looks like tracking wired to two providers. It was wired to neither. package.json had no analytics dependency. The root layout's only inline scripts were JSON-LD structured data and, later, a theme bootstrap. Nothing on the page ever defined window.plausible or window.va. The setup looked finished in two places: .env.example set NEXT_PUBLIC_PLAUSIBLE_DOMAIN=abdur.ai under a comment reading "Plausible or Vercel Analytics — pick one", and the file's doc comment said to "wire to Plausible/Vercel Analytics in lib/analytics.ts". That file didn't exist yet.
The code stayed like that until September 4. I can't tell you how many clicks it lost. That's the point: nothing recorded them.
Why nothing complained
Three layers each did their job exactly as written, and together they made the failure invisible.
Optional chaining swallowed the runtime miss. window.plausible?.(name) means "call this if it exists." When it doesn't exist, the expression evaluates to undefined. No exception, no console warning, nothing for an error tracker to catch. A missing provider looks the same as a working one that returned nothing.
@ts-expect-error silenced the compiler, which had it right. TypeScript knew plausible and va weren't declared on Window, and it would have said so on every build. The directive tells the compiler to expect an error on the next line and keep quiet about it. It even fails the build if that error ever goes away. So on every build, the type checker was confirming that nothing declared these globals, and the comment kept that confirmation out of sight.
The config looked done. An env var named for a provider, a comment pointing at a wrapper file. Both read as "this is set up." Neither was checked by anything.
Who this bites
Anyone who adds analytics call sites before the provider exists, which is a normal order to build in. You write the CTA, add a track() call so it's ready, and plan to wire the provider later. It also bites code you inherit or generate: a ?.() on a vendor global plus a type suppression is a very plausible-looking pattern, and it passes lint, typecheck, and build.
What I changed
The fix is commit e8c24ac (task A-1, September 4):
- Installed a real provider.
@vercel/analyticswent intopackage.json, and<Analytics />went intoapp/layout.tsx. - One typed wrapper, no globals.
lib/analytics.tsexportstrackEvent(), which importstrackfrom the SDK. No call site toucheswindow.*anymore. That moves the failure: if the package goes missing, the import fails the build instead of quietly doing nothing. - Checked success before counting it. One waitlist form marked itself done on any response.
fetchdoesn't reject on a 4xx or 5xx, only on a network failure, so a failed signup showed the success state. It now checksres.ok, and its conversion event fires only after a 2xx.
A day later, 6c284f7 added two more things. Event names are now typed against a catalog (ANALYTICS_EVENTS), so a misspelled event is a compile error instead of a new, empty series in the dashboard. And the README now documents a second silent drop I would otherwise have walked into: Vercel custom events need a Pro plan or higher. On Hobby, track() calls are accepted and dropped, and the dashboard shows pageviews and nothing else. That reads as "no conversions," not "not recorded."
What I haven't proven yet
The fix is code-side. The build-plan row for A-1 says dashboard verification happens after deploy: enable Analytics in the Vercel project and confirm events land. Until one known event shows up in the dashboard, "tracking works" is the same unproven claim the old code was making. The wrapper also can't see an ad blocker. The SDK's track is a no-op when its script is blocked, so counts will under-report. The README says to treat them as directional.
How to check your own site
- Open the production page and ask the global directly. In the browser console, run
typeof window.plausible(or whichever global your call sites use). If it says"undefined", every call to it does nothing. - Search for the pattern. Grep for
window.followed by?.(, and for@ts-expect-errororas anynext to analytics calls. Each hit is a place where a missing provider turns into silence. - Import the SDK; don't reach for a global. A module import fails the build when the package is missing. A
windowlookup never fails. - Type your event names. A const list and a union type make a typo a compile error.
- Check the plan and the dashboard setting. Custom events can be gated by plan or switched off in the project, and both fail quietly.
- Fire one known event and find it in the dashboard. An empty events chart is only data after you've seen a test event arrive.
FAQ
Why are my analytics events not showing up when there are no errors?
The most common silent cause is that the provider's script never loaded, and the call site is written so a missing function doesn't throw. window.provider?.() and if (window.provider) window.provider() both do nothing when the global is undefined. Check the global in the console on the live page first.
Does optional chaining hide errors?
It hides exactly one: calling something that doesn't exist. a?.() returns undefined when a is null or undefined, without calling anything. That's right for optional features. It's wrong for a call you expect to always run, because it turns "misconfigured" into "fine."
Why didn't TypeScript catch it?
It did. The @ts-expect-error comment told it to suppress the error. Declaring the global with declare global or casting with as any would hide it the same way. If you need a vendor global, declare its type in one place and check at runtime that it exists. Better still, use the vendor's package.
Why don't my Vercel Analytics custom events appear?
Check three things. Is Web Analytics enabled for the project? Is the team on a plan that includes custom events? This repo's README records that they need Pro or higher and are dropped silently on Hobby. And is the script being blocked in your own browser? Pageviews appearing while events stay empty points at the plan or the call site, not the script.
Does fetch throw on a 404 or 500?
No. fetch resolves for any HTTP response and rejects only on network failure. Check res.ok (or res.status) before you treat a request as a success, and before you fire a conversion event for it.
The broader lesson is the one from the CI gate nothing ran: a check that can't fail looks exactly like a check that passes. This call couldn't fail, so for nearly ten weeks it passed.