You shipped a Next.js 14 app router site, the /demo route looks fine in QA, and yet only 1 in 4 form opens turns into a submission. Server logs are clean. There's no error in Sentry. Something is happening in the browser between "user clicked the email field" and "user closed the tab," and the only way to see it is to record what they did.
This is the install guide I wish existed when I first wired a session tracker into an app router project. The big footgun isn't the SDK — it's that the root layout.tsx runs on the server, so naive snippets either fail to mount or kill streaming. Below is the 20-minute version that works.
What we're actually setting up
Two things, in this order:
- The CloseTrace tracker — captures clicks, scrolls, navigations, and the DOM mutations needed for replay.
- Lead recovery — captures form field values as the user types so half-filled forms aren't lost when someone closes the tab.
If you just want replay, stop after step 1. If you have a demo form, contact form, or pricing-quote flow, do both. Lead recovery is the thing that pays for itself on a B2B site — most teams find more pipeline in week one than they spent on the tool.
Install the tracker
Grab your public key from CloseTrace → Settings → Setup (the ct_pk_... string). Then add an env var:
# .env.local
NEXT_PUBLIC_CLOSETRACE_KEY=ct_pk_xxxxxxxxxxxxxxxxxxxxxxxx
The NEXT_PUBLIC_ prefix is required — the key has to reach the browser bundle. It's a public key, not a secret, so this is intentional.
Mount it from the root layout
In app router, the root layout.tsx is a server component by default. You can't just drop a script tag with inline JS into it the way you would in pages router. Use next/script with the afterInteractive strategy:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script
id="closetrace"
strategy="afterInteractive"
src={`https://api.closetrace.com/t.js?k=${process.env.NEXT_PUBLIC_CLOSETRACE_KEY}`}
/>
</body>
</html>
);
}
A few things worth knowing:
afterInteractiveloads after hydration. That's the right strategy here — you want the tracker after React is interactive, not blocking First Contentful Paint.- Don't use
beforeInteractive. It only works in the root layout, and forcing the tracker to load that early measurably hurts LCP on slower mobile devices. - Don't put this in a per-route layout. Sessions need to span page transitions; if you mount the script in
/app/(marketing)/layout.tsx, you lose continuity when users navigate to/app/(app)/....
Deploy this, open the site in a private window, click around, and check the "Live sessions" view in the CloseTrace dashboard. You should see your own session within about 10 seconds.
The current browser bundle does not expose a CloseTrace('init'), identify, or general custom track API. The public key must be in the script URL exactly as shown above. Eligible form interaction is associated with the captured session automatically; supported values are included only when the active workspace policy permits them.
Turn on lead recovery
This is the feature most teams underuse. CloseTrace detects eligible named form fields automatically and attaches permitted form-draft updates to the active session; no identify API is required or currently exposed.
Use a stable form ID and ordinary named fields. Eligible lead forms are detected automatically:
// app/demo/page.tsx (or wherever your form lives)
<form id="demo-form" onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="company" />
<textarea name="use_case" />
<button type="submit">Request demo</button>
</form>
Replay applies the active workspace privacy policy before upload. Password, payment, authentication, SSN, date-of-birth, bank-account, and similarly named fields are permanently excluded from form drafts. Mark any additional private field or container with data-private.
In the dashboard, open Lead Recovery and filter the captured drafts for demo-form. That's the list to triage on a Monday morning — pair an eligible lead field with the replay and you'll usually see why they stopped (broken phone validation, a captcha that won't load, a hidden submit button on mobile — we keep a running list of the common ones).
App Router navigation
The tracker wraps history.pushState and replaceState and listens for popstate, so client-side App Router navigations create page-view events without remounting the script. Keep the snippet in the root layout and do not add a second copy to nested layouts.
Verify the install in 60 seconds
- Open your site in a fresh private window.
- Type a fake email into the
demo-form, then close the tab without submitting. - In the CloseTrace dashboard, open the session and confirm the form interaction appears in Form drafts. A supported value appears only when the active workspace capture policy permits it.
If step 3 works, you're done. The next thing to do is set up a funnel from your landing route to the form submit event so you can see the abandonment rate trend, not just individual sessions. That's a 5-minute job and it's where the recurring value lives.
