Trackers
Two ready-made components call track for you: TrackPageViews records page views and
TrackErrors records uncaught errors. For the errors they can't see, trackError records a
thrown value from your own code. All three come with the Next.js implementation,
@eventrail/nextjs; names and behaviour on this page are those of that package.
Add the trackers
Mount each once, near the root of your app, and pass it the browser producer's track. Neither
renders anything.
Next.js · app/layout.tsx
import { TrackErrors, TrackPageViews } from "@eventrail/nextjs/client";
import { track } from "@/lib/eventrail-client";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<TrackPageViews track={track} />
<TrackErrors track={track} />
</body>
</html>
);
}In Next.js, both are Client Components already, so your root layout stays a Server Component.
To hold the trackers' events until the visitor answers the consent question, see Blocking consent banner. To record only signed-in users, mount the trackers conditionally, as in Authenticated users only.
Page views
TrackPageViews records a page_view when it mounts, which in the root layout is the page load,
and again every time the URL changes, whatever caused it:
| What the visitor does | Recorded |
|---|---|
Opens /pricing from a Google search | A page_view with referrer: "https://www.google.com/" |
Clicks through to /features | A page_view with referrer: "https://yourapp.com/pricing" |
Switches from /settings?tab=billing to /settings?tab=team | A page_view |
Jumps from /features to /features#faq | A page_view |
| Goes back or forward | A page_view |
| Follows a link to the page they are already on | Nothing: the URL didn't change |
Changes made outside your framework's router count too, such as another script calling
history.pushState. The record's location is the full page URL, query string and hash included.
Every page_view carries a referrer property:
- The first one after the tracker mounts holds the page the visitor came from, as the browser
reports it in
document.referrer. It isnullwhen there is none, for example when the visitor typed the URL or used a bookmark. Browsers by default, and most sites, send only the origin to another site: a visitor from a Google search arrives withhttps://www.google.com/, not the search URL. - Every one after it holds the full URL of the previous page view. A visit's entry points are
the page views whose
referreris not on your own origin.
Mount the tracker where it stays mounted for the whole visit. Each time it mounts again, for
example in a layout that visitors navigate out of and back into, its first page_view carries the
page load's referrer again.
Records as a destination receives them, trimmed to the fields the tracker sets:
From a search
{
name: "page_view",
location: "https://yourapp.com/pricing",
producerProperties: { referrer: "https://www.google.com/" }The first page view after the page load.
}Typed URL
{
name: "page_view",
location: "https://yourapp.com/pricing",
producerProperties: { referrer: null }No referrer to report: null, not an empty string.
}Client-side navigation
{
name: "page_view",
location: "https://yourapp.com/settings?tab=team",Only the query string changed: still a page view.
producerProperties: { referrer: "https://yourapp.com/settings?tab=billing" }The previous page view, on your own origin.
}Uncaught errors
TrackErrors records an error event for every error that reaches window:
- an uncaught exception, such as a throw in a click handler or a timer;
- an unhandled promise rejection, such as a failed
fetchthat nothing catches.
| Property | What it holds |
|---|---|
message | The error message, cut to 4,096 characters. For an uncaught exception, it is the browser's wording: Chrome starts it with Uncaught. |
errorName | The error's name, such as "TypeError". null when the thrown value isn't an Error. |
stack | The stack trace, cut to 2,000 characters. null when the thrown value isn't an Error. |
source | "uncaughtException" or "unhandledPromiseRejection". |
filename, lineno, colno | Where an uncaught exception was thrown. Rejections don't have them. |
A thrown value that isn't an Error is recorded as JSON in message: Promise.reject("timeout")
becomes message: '"timeout"'.
Some errors are left out, or arrive without details:
- After 20 errors, the tracker stops recording until it mounts again, which in the root layout is the next page load. An error that repeats in a loop doesn't flood your table.
- EventRail's own errors are skipped, such as a
setConsent()failure nobody caught. They go to the producer'sonErrorinstead. - A script from another origin, such as a chat widget or an ad, that throws arrives as
message: "Script error."with everything elsenull. The browser hides the details unless the script is loaded with CORS. - Errors an error boundary catches never reach
window. See Errors an error boundary catches.
Uncaught exception
{
name: "error",
location: "https://yourapp.com/settings?tab=billing",
producerProperties: {
message: "Uncaught TypeError: Cannot read properties of undefined (reading 'last4')",The browser's wording of the message, with Chrome's Uncaught prefix.
errorName: "TypeError",
stack: "TypeError: Cannot read properties of undefined (reading 'last4')\n at onClick (https://yourapp.com/_next/static/chunks/app/settings/page-3f2a1c.js:1:4821)",
source: "uncaughtException",
filename: "https://yourapp.com/_next/static/chunks/app/settings/page-3f2a1c.js",
lineno: 1,
colno: 4821
}
}Unhandled rejection
{
name: "error",
location: "https://yourapp.com/dashboard",
producerProperties: {
message: "Failed to fetch",A fetch that failed on the network, with no catch.
errorName: "TypeError",
stack: "TypeError: Failed to fetch\n at loadInvoices (https://yourapp.com/_next/static/chunks/app/dashboard/page-9b4e7d.js:1:2210)",
source: "unhandledPromiseRejection"
}
}Script from another origin
{
name: "error",
location: "https://yourapp.com/pricing",
producerProperties: {
message: "Script error.",All the browser tells a page about another origin's error.
errorName: null,
stack: null,
source: "uncaughtException",
filename: null,
lineno: null,
colno: null
}
}Errors an error boundary catches
An error boundary catches errors thrown while rendering, so they never reach window and
TrackErrors doesn't see them. Record them in the boundary with trackError. In Next.js, the
boundaries are error.tsx and, for errors in the root layout, global-error.tsx:
Next.js · app/error.tsx
"use client";
import { useEffect } from "react";
import { trackError } from "@eventrail/nextjs/core";
import { track } from "@/lib/eventrail-client";
export default function ErrorPage({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
useEffect(() => {
trackError(track, error, { source: "errorBoundary", digest: error.digest ?? null });Your own source, to tell these apart from what TrackErrors records.
}, [error]);
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={() => retry()}>Try again</button>
</div>
);
}Put the same call in global-error.tsx. In a production build, Next.js replaces the message of an
error thrown in a Server Component with a generic one, so the digest is what matches the record
to your server logs:
{
name: "error",
location: "https://yourapp.com/invoices/inv_1042",
producerProperties: {
message: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.",The generic message: the real one is only in your server logs.
errorName: "Error",
stack: "Error: An error occurred in the Server Components render. …",
source: "errorBoundary",
digest: "2338951184"Search your server logs for this value.
}
}Errors you catch yourself
An error your code catches and handles reaches neither the tracker nor a boundary. To record it
anyway, call trackError(track, error, errorProperties) in the catch. It takes either
producer's track, so an error event from the server looks the same as one from the browser:
Next.js · app/checkout/actions.ts
"use server";
import { trackError } from "@eventrail/nextjs/core";
import { track } from "@/lib/eventrail-server";
import { sendReceipt } from "@/lib/email"; // your code
export async function emailReceipt(orderId: string) {
try {
await sendReceipt(orderId);
} catch (error) {
trackError(track, error, { orderId });The order went through: a missing receipt shouldn't fail the request.
}
}{
name: "error",
location: "https://yourapp.com/checkout",A server event: the Referer of the request, the page that called the action.
producerType: "local",
producerProperties: {
message: "Mail server did not respond in 10000 ms",
errorName: "TimeoutError",
stack: "TimeoutError: Mail server did not respond in 10000 ms\n at sendReceipt (/app/.next/server/chunks/412.js:1:3380)",
orderId: "ord_1042"Recorded because the call named it.
}
}- Only
message,errorNameandstackare read from the thrown value. Anything else it carries, such as a status code, is recorded only when you name it inerrorProperties, so you decide at the call site what leaves your app. errorPropertiesare merged over those three, so amessageof your own replaces the error's.messageis cut to 4,096 characters (LIMITS.maxStringLength) either way.