EventRail
Docs menu

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.

app/layout.tsx

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 doesRecorded
Opens /pricing from a Google searchA page_view with referrer: "https://www.google.com/"
Clicks through to /featuresA page_view with referrer: "https://yourapp.com/pricing"
Switches from /settings?tab=billing to /settings?tab=teamA page_view
Jumps from /features to /features#faqA page_view
Goes back or forwardA page_view
Follows a link to the page they are already onNothing: 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 is null when 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 with https://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 referrer is 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.
}

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 fetch that nothing catches.
PropertyWhat it holds
messageThe error message, cut to 4,096 characters. For an uncaught exception, it is the browser's wording: Chrome starts it with Uncaught.
errorNameThe error's name, such as "TypeError". null when the thrown value isn't an Error.
stackThe stack trace, cut to 2,000 characters. null when the thrown value isn't an Error.
source"uncaughtException" or "unhandledPromiseRejection".
filename, lineno, colnoWhere 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's onError instead.
  • A script from another origin, such as a chat widget or an ad, that throws arrives as message: "Script error." with everything else null. 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
  }
}

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:

app/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:

app/checkout/actions.ts

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, errorName and stack are read from the thrown value. Anything else it carries, such as a status code, is recorded only when you name it in errorProperties, so you decide at the call site what leaves your app.
  • errorProperties are merged over those three, so a message of your own replaces the error's. message is cut to 4,096 characters (LIMITS.maxStringLength) either way.