Install
Add analytics and revenue attribution to Next.js
Two ways to install: the script tag, which takes a minute, or the first-party proxy, which takes ten and keeps working for the third of your audience running an ad blocker. If you deploy on Vercel, read the proxy section — a plain rewrite silently loses visitor geography.
Drop the snippet into the root layout
In the App Router, put it in app/layout.tsx. Next renders a plain <script> in the body without hoisting or deduping it, which is what you want here — no next/script strategy needed, since the tag is already deferred. Your site key is in Dashboard → Sites → the </> icon.
app/layout.tsxexport default function RootLayout({ children }) { return ( <html lang="en"> <body> {children} <script defer src="https://revtrail.pyln.dev/track.js" data-site="SITE_KEY"></script> </body> </html> ); }Serve it first-party so ad blockers don't eat your data
Blockers match on the third-party domain, not on what the script does. Rewrite a path on your own domain to our script and the request looks like any other asset you serve. This is worth doing on day one — retrofitting it later means a visible step change in your numbers.
next.config.jsmodule.exports = { async rewrites() { return [{ source: "/rt/track.js", destination: "https://revtrail.pyln.dev/track.js" }]; }, }; // then load it from your own origin: // <script defer src="/rt/track.js" data-site="SITE_KEY"></script>Relay the beacon through a route handler (Vercel: required for geo)
The script also POSTs events, and those need a first-party path too. Don't use a plain rewrite for this one: on Vercel an external rewrite drops the headers we derive country from, so every visitor lands as unknown. A route handler that forwards user-agent, x-forwarded-for, and the Vercel geo header keeps geography intact.
app/api/fn/ingestEvent/route.tsexport async function POST(request: Request) { const body = await request.text(); const res = await fetch("https://revtrail.pyln.dev/api/fn/ingestEvent", { method: "POST", headers: { "content-type": "application/json", "user-agent": request.headers.get("user-agent") ?? "", "x-forwarded-for": request.headers.get("x-forwarded-for") ?? "", "cf-ipcountry": request.headers.get("x-vercel-ip-country") ?? "", }, body, }); return new Response(await res.text(), { status: res.status }); }Confirm it's receiving
Open your site in a normal browser tab, then check Realtime in the Revtrail dashboard — your own visit should appear within a few seconds. If nothing arrives, the usual causes are a mistyped site key, an ad blocker on your own browser, or a Content-Security-Policy that blocks the script origin.
Instrument the conversion that matters
Pageviews tell you traffic; a goal tells you whether the traffic worked. Call revtrail() at the moment of signup, trial start, or booking, then register it as a goal in the dashboard. This is the one step people skip, and it's the step that makes every other number worth reading.
anywhere in your client coderevtrail('signup')Wire revenue in from Stripe
Point a Stripe webhook at Revtrail for checkout.session.completed and invoice.payment_succeeded, save the signing secret in your site settings, and pass the visitor id as the Checkout session's client_reference_id. Payments then attach to the visitor's first-touch channel, including renewals months later. Revenue is never accepted from the browser, so this webhook is the only way money reaches your dashboard.
when you create the Checkout sessionconst visitorId = await revtrail.visitorIdAsync(); // best-effort: omit when null, never block checkout on analytics stripe.checkout.sessions.create({ client_reference_id: visitorId ?? undefined, // …line items, success_url, etc. });
App Router and Pages Router both work unchanged
The snippet is a plain script tag with no framework coupling — no provider, no hook, no client component. In the Pages Router put it in pages/_document.tsx instead of app/layout.tsx. Client-side route changes are picked up automatically: the script patches history.pushState and fires a pageview on navigation, so Next's soft transitions are counted without an effect in your code.
Server components and streaming don't change anything
Tracking happens entirely in the browser after hydration, so RSC, streaming, and partial prerendering are irrelevant to it. There's nothing to opt out of and no dynamic-rendering penalty: a fully static page still reports its pageviews.
The visitor id and Stripe Checkout
Use await revtrail.visitorIdAsync() rather than the synchronous revtrail.visitorId(), which returns null until the first beacon comes back. On an instant-checkout page the sync call reliably loses that race, and the payment lands unattributed. Treat it as best-effort — pass undefined when it's null and never block a sale on an analytics call.
Questions people actually ask
- Should I use next/script instead of a plain script tag?
- You don't need to. The tag is already deferred, and next/script's strategies mainly help with scripts that would otherwise block. A plain tag in the layout is simpler and behaves identically here.
- Why does the beacon need a route handler instead of a rewrite?
- On Vercel, an external rewrite doesn't carry through the headers we use to resolve country, so geography comes back empty. The route handler forwards user-agent, x-forwarded-for, and x-vercel-ip-country explicitly.
- Do I need a cookie banner for this?
- Not for Revtrail. The default identity is a daily rotating hash — no cookies, no persistent identifier, nothing stored on the device. If you run ad pixels or other analytics that do use cookies, their obligations are unchanged.
- Will this slow my site down?
- The script is deferred, so it never blocks rendering, and it's about 3KB over the wire gzipped. It fires one beacon per pageview.
Install guides