Analytics & Feedback
Optional PostHog analytics and a feedback link, plus Speed Insights on every Vercel deploy.
PostHog and the feedback link are optional and stay off until you set an environment variable, so out of the box neither one runs and you opt in when you need them. Speed Insights works the other way round: it takes no key and no variable, and it renders on every Vercel deployment, so you turn it off by removing it, not on by adding something.
PostHog analytics
PostHog gives you product analytics, session replay, and feature flags. It is wired but dormant: initialized in web/next/instrumentation-client.ts (Next.js's client-instrumentation entry, which runs once before the app hydrates), following PostHog's current App Router setup. Its defaults date turns on PostHog's current defaults, including single-page-app pageview capture, so no provider or manual pageview component is needed. It no-ops until the key is present.
Turn it on with two env vars (the key enables it, the host picks your region):
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=Use https://us.i.posthog.com for US Cloud, https://eu.i.posthog.com for EU, or your self-hosted URL. Both are declared optional in packages/env/src/web-next.ts, so an empty key simply means PostHog never initializes, which is also how you disable it.
Wired, but not called for you
The starter sets up initialization, but it does not call capture, identify, or read a feature flag anywhere. Pageviews and session replay begin once the key is set; everything below is yours to add where it makes sense.
Capture events and identify users from the shared client:
import posthog from "posthog-js"
posthog.capture("signup_clicked", { plan: "pro" })
posthog.identify(user.id, { email: user.email, name: user.name })Inside components, reach the same singleton and read flags off it (isFeatureEnabled resolves once flags have loaded):
import posthog from "posthog-js"
function Dashboard() {
const showNew = posthog.isFeatureEnabled("new-dashboard")
return showNew ? <New onClick={() => posthog.capture("used_new")} /> : <Old />
}PostHog is privacy-configurable: the host picks EU or US data residency, session recordings can be masked or turned off, and its built-in opt-out honors users who decline tracking.
Vercel Speed Insights
Speed Insights reports real Core Web Vitals from your actual visitors, rather than the synthetic numbers a Lighthouse run gives you. It needs no environment variable and no key, because it only reports to the platform it is deployed on.
That is also why it is conditional. The beacon posts to /_vercel/speed-insights/vitals, a route only Vercel serves, so on Docker or any self-host it would fire requests that can never arrive. The root layout gates it on the platform instead:
// web/next/src/app/layout.tsx
const onVercel = process.env.VERCEL === "1"<body className="min-h-svh">
<InnerProvider>
<Navbar />
{children}
</InnerProvider>
{onVercel && <SpeedInsights />}
</body>VERCEL is injected by the platform rather than configured by you, which is why it appears in neither packages/env nor .env.example. The check has to stay in the layout, a server component: SpeedInsights is a "use client" component, and Next only inlines NEXT_PUBLIC_* variables into client bundles, so the same check inside a client component would read undefined and silently never render.
What the gate does and does not do: off Vercel the component is never rendered, so the beacon script never loads and no request is made. The package's own module is a static import, so it still ships in the client bundle either way, where nothing executes it. Make it a dynamic import if you want it gone from the bundle too.
There is no enable step. Deploy to Vercel and it starts collecting, because every deployment already serves the collection routes; the numbers appear under Speed Insights in the project dashboard once real visitors have been through. Nothing else to wire.
Previews count against your quota
Every deployment reports, previews included, and Vercel meters Speed Insights by data point. Narrowing to process.env.VERCEL_ENV === "production" drops PR previews, but it drops canary too, since canary deploys as a preview. To keep canary and lose the rest, test the branch as well, the way .github/scripts/migrate-on-deploy.ts already picks its deploy branches:
const onVercel =
process.env.VERCEL_ENV === "production" || process.env.VERCEL_GIT_COMMIT_REF === "canary"To remove it entirely, delete the import, the onVercel line with its comment, and the one line that renders it, then drop @vercel/speed-insights from web/next/package.json and the root catalog.
User feedback
The feedback link is simpler still (one env var, no SDK):
NEXT_PUBLIC_USERJOT_URL=The name points at UserJot, but the value is just a URL: set it to any feedback destination you like. When it is set, a link appears in two places: the docs sidebar footer (next to the v{version} label) and the dashboard user menu, directly above "Log out". Both open the URL in a new tab. Leave the variable empty and neither renders; the footer still shows the version.
Next
- Environment Variables: how these optional keys are declared and validated.
- Dashboard: the app shell where the feedback link and user menu live.