Next.js
@caravela-studio/glancy-next covers both sides of a Next.js app: a provider that tracks page views and gives your
components track / identify, a middleware that tracks from the server, and a server client for route handlers and
server actions. It works with the App Router and the Pages Router.
Install
npm install @caravela-studio/glancy-next
Add your Hey Glancy address and write key to .env.local (the write key is on your bucket's Setup page):
NEXT_PUBLIC_GLANCY_HOST=https://ingest.heyglancy.com
NEXT_PUBLIC_GLANCY_WRITE_KEY=YOUR_WRITE_KEY
The write key can only add events to one bucket, never read them, so it's fine for it to reach the browser.
Browser
Wrap your app in GlancyProvider. It sends a page view on every route change:
// app/layout.tsx
import { GlancyProvider } from "@caravela-studio/glancy-next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GlancyProvider>{children}</GlancyProvider>
</body>
</html>
);
}
Then use useGlancy() in any client component:
"use client";
import { useGlancy } from "@caravela-studio/glancy-next";
export function BuyButton() {
const glancy = useGlancy();
return <button onClick={() => glancy.track("Order Completed", { revenue: 42, currency: "EUR" })}>Buy</button>;
}
After someone signs in, identify them; on sign out, reset:
glancy.identify(user.id, { name: user.name, email: user.email });
glancy.reset();
Provider props
| Prop | Default | |
|---|---|---|
writeKey, host |
the env vars above | |
trackPageViews |
true |
One page view per route change |
sessionReplay |
false |
true or options; see Session replay |
heatmaps |
false |
Clicks and scroll depth |
flushAt / flushInterval |
20 / 2000 ms |
Batching |
debug |
false |
Log every message |
<GlancyProvider sessionReplay={{ sampleRate: 0.25 }} heatmaps>{children}</GlancyProvider>
Methods
| Method | What it does |
|---|---|
track(event, properties?) |
Sends an event |
page(name?, properties?) |
Sends a page view (automatic with the provider) |
identify(userId, traits?) |
Ties this browser to a user and updates their profile |
group(groupId, traits?) |
Ties the user to a company or team |
alias(userId, previousId?) |
Merges an old id into a new one |
reset() |
Forgets the user; new anonymous id and session |
flush() |
Sends the queue now |
stopRecording() |
Stops replay and heatmaps (consent withdrawn); events keep working |
getSessionId() |
The current session id |
Middleware
withGlancy() tracks from the server, in one file, without touching your routes:
// middleware.ts
import { withGlancy } from "@caravela-studio/glancy-next/middleware";
export default withGlancy();
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };
It reads GLANCY_HOST and GLANCY_WRITE_KEY (or the NEXT_PUBLIC_ ones), and:
- Sends a page view for every page load from the server, so landing pages count even when an ad blocker stops
the browser script. The browser SDK reuses the same page view id, so with both in place each page view is
stored once, with the browser's details (title, screen size) filled in. Client-side navigations are tracked by
<GlancyProvider>as before: Next.js doesn't let middleware tell a navigation from a prefetch.context.sourcesays who saw each page view:server,browserorbrowser+server. Break a report down by it to see how many visits only the server caught (usually an ad blocker). - Sends an
API Requestevent (method and path) for requests under/api/. Middleware runs before your route answers, so it only addsstatuswhen your own middleware answered (a 401, say). For every status, usewithGlancyRoute()below. - Sets the anonymous id cookie on the first request, so server and browser events belong to the same visitor from the very first hit.
- Skips static files, prefetches and redirects, and sends in the background so responses aren't slowed down.
Secret URL parameters (
token,code…) are redacted, and crawlers are dropped by Hey Glancy.
| Option | Default | |
|---|---|---|
pageViews |
true |
Page views for page loads |
api |
["/api/"] |
Path prefixes (or regular expressions, or functions of the request) sent as API Request; false for none |
exclude |
[] |
Requests never tracked, e.g. ["/api/health"] |
userId |
A function of the request returning the signed-in user's id, so events land on their profile | |
writeKey, host, onError |
env vars, console.error |
As for the server client |
export default withGlancy({
exclude: ["/api/health"],
userId: (req) => req.cookies.get("uid")?.value,
});
Already have a middleware? Pass it as the second argument. It runs first, and its response is kept:
import { NextResponse } from "next/server";
import { withGlancy } from "@caravela-studio/glancy-next/middleware";
export default withGlancy({}, (req) => {
if (!req.cookies.has("session") && req.nextUrl.pathname.startsWith("/account")) {
return NextResponse.redirect(new URL("/login", req.url));
}
});
Status codes and durations
Wrap a route handler in withGlancyRoute() to send its API Request with the response's status and how long it
took (duration_ms). A handler that throws counts as a 500, and the error goes on as before. Then filter or break
down by status (≥ 500 for server errors) in Insights, Funnels and the rest.
// app/api/orders/route.ts
import { withGlancyRoute } from "@caravela-studio/glancy-next/server";
export const POST = withGlancyRoute(async (req) => Response.json(await createOrder(await req.json()), { status: 201 }), {
userId: (req) => req.headers.get("x-user-id"),
});
Not on Next.js? The same wrapper, and an Express middleware, are in @caravela-studio/glancy-web/server.
It takes event (default API Request), userId, writeKey, host and onError, and sends in the background
with after() on Next.js 15.1 and later. With the middleware too, set api: false there (or exclude the wrapped
routes) so a request isn't counted twice.
An API Request event is a request, not an action: it's useful for usage and spotting busy endpoints. Send the
events that matter (Signed Up, Order Completed) from your route handlers, with the user's id, as below.
Server
Some events are better sent from the server: they can't be blocked by ad blockers, and they happen where the truth
is (a payment confirmed, a subscription renewed). Set GLANCY_HOST and GLANCY_WRITE_KEY on the server (the
NEXT_PUBLIC_ ones are used if they're missing), then:
// app/api/checkout/route.ts
import { glancy, getAnonymousId } from "@caravela-studio/glancy-next/server";
import { cookies } from "next/headers";
import { after } from "next/server";
export async function POST() {
const anonymousId = getAnonymousId(await cookies());
after(() =>
glancy().track({ userId: user.id, anonymousId, event: "Order Completed", properties: { revenue: 42 } }),
);
return Response.json({ ok: true });
}
- Every server call (
track,identify,page,group,alias,batch) needs auserIdor ananonymousId. getAnonymousId()reads the browser's anonymous id from theglancy_anon_idcookie, so server events land on the same visitor as their browser events, even before they sign in.- Each call is sent right away and resolves once Hey Glancy has stored it, so serverless functions don't drop events.
Wrapping it in
after()keeps it off your response time. - Sending never throws. Failures go to
onError(by defaultconsole.error). - For explicit config, create a client:
new ServerClient({ writeKey, host }).