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.source says who saw each page view: server, browser or browser+server. Break a report down by it to see how many visits only the server caught (usually an ad blocker).
  • Sends an API Request event (method and path) for requests under /api/. Middleware runs before your route answers, so it only adds status when your own middleware answered (a 401, say). For every status, use withGlancyRoute() 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 a userId or an anonymousId.
  • getAnonymousId() reads the browser's anonymous id from the glancy_anon_id cookie, 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 default console.error).
  • For explicit config, create a client: new ServerClient({ writeKey, host }).