Better Stack Next.js client

Start logging in 6 minutes

1. Install

Install Logtail Next.js NPM package:

Install Logtail Next.js
npm install @logtail/next

2. Setup

Set environment variables, e.g. in your console before running the server:

Set environment variable
export NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN="$SOURCE_TOKEN"
export NEXT_PUBLIC_BETTER_STACK_INGESTING_URL="https://$INGESTING_HOST"


Set up Better Stack Next.js client in next.config.ts or next.config.js:

Wrap Next.js config with Better Stack
import { withBetterStack } from '@logtail/next';

export default withBetterStack({
  // Your existing config
});

3. Start logging 🎉

Use structured logging in client, edge, or server-side files.

You should see your logs in Better Stack → Live tail.

Using older versions of Next.js?

For Next.js version 14, use @logtail/next in 0.2.*. For slight differences in syntax, see the "in Next.js v14" tab in examples below.

Older versions 12.1.4 and above are supported in @logtail/next version 0.1.*. See legacy docs.

Route handlers

Wrap your route handlers in withBetterStack to add a logger to your request and log exceptions automatically:

Wrap route handler with Better Stack in Next.js v14
import { withBetterStack, BetterStackRequest } from '@logtail/next';

export const GET = withBetterStack((request: BetterStackRequest) => {
  request.log.info('Login function called');

  // You can create intermediate loggers
  const log = request.log.with({ scope: 'user' });
  log.info('User logged in', { userId: 42 });

  return NextResponse.json({ hello: 'world' });
});
import { withBetterStack, BetterStackRequest } from '@logtail/next';

export const GET = withBetterStack((req: BetterStackRequest) => {
  req.log.info('Login function called');

  // You can create intermediate loggers
  const log = req.log.with({ scope: 'user' });
  log.info('User logged in', { userId: 42 });

  return NextResponse.json({ hello: 'world' });
});

Client components

To send logs from client components, add useLogger to your component:

Send logs from client components to Better Stack in Next.js v14
'use client';
import { useLogger } from '@logtail/next/hooks';

export default function ClientComponent() {
  const log = useLogger();
  log.debug('User logged in', { userId: 42 });
  return <h1>Logged in</h1>;
}
'use client';
import { useLogger } from '@logtail/next';

export default function ClientComponent() {
  const log = useLogger();
  log.debug('User logged in', { userId: 42 });
  return <h1>Logged in</h1>;
}

Server components

To send logs from server components, add Logger to your component, and call flush before returning:

Send logs from server components to Better Stack
import { Logger } from '@logtail/next';

export default async function ServerComponent() {
  const log = new Logger();
  log.info('User logged in', { userId: 42 });

  // ...

  await log.flush();
  return <h1>Logged in</h1>;
}

Configuring authentication middleware

If your Next.js application uses authentication middleware (like Clerk or NextAuth.js), it may block telemetry requests sent from the browser. These requests, sent to the /_betterstack/* proxy route, are essential for collecting client-side logs and Web Vitals.

To prevent issues, you must configure your middleware to treat /_betterstack/(.*) as a public or ignored route.

Here are configuration examples for common middleware libraries:

middleware.ts (Clerk) middleware.ts (NextAuth.js)
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/_betterstack/(.*)', // Allow Better Stack telemetry
]);

export default clerkMiddleware((auth, request) => {
  // Skip authentication for public routes
  if (isPublicRoute(request)) {
    return;
  }

  // Protect all other routes
  auth().protect();
});

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
export { default } from "next-auth/middleware"

export const config = { 
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - _betterstack (Better Stack telemetry)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|_betterstack).*)',
  ]
}

By correctly configuring your middleware, you ensure that all telemetry data from the browser is successfully collected.

Keep telemetry failures out of your app

By default, withBetterStack proxies browser telemetry to Better Stack using a Next.js rewrite. If the ingest endpoint is slow or unreachable, Next.js turns the failed rewrite into a 500 error on your own domain.

To make delivery failures invisible to the browser, serve the proxy path with the route handler included in @logtail/next version 0.4.0 or higher. It forwards telemetry with a 5-second timeout and always responds with 204, logging delivery failures on the server only.

Pick a proxy path that can host a route handler. The default /_betterstack can't, as folders prefixed with _ are private in the App Router:

Set the proxy path in .env
NEXT_PUBLIC_BETTER_STACK_PROXY_PATH=/betterstack

Export the route handler for both proxied endpoints:

app/betterstack/logs/route.ts
// Create the same file in app/betterstack/web-vitals/route.ts
export { POST } from '@logtail/next/proxy';

Static route files take precedence over the rewrite, so nothing else changes. Use one file per endpoint, as a [...path] catch-all would be shadowed by the rewrite.

Using authentication middleware? Allow your new proxy path instead of /_betterstack in the middleware configuration.

Need help?

Please let us know at hello@betterstack.com.
We're happy to help! 🙏

Advanced usage

Capture traffic requests

To capture traffic requests, create a middleware.ts file in the root folder of your Next.js app:

Capture traffic requests using middleware
import { Logger } from '@logtail/next'
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export async function middleware(request: NextRequest, event: NextFetchEvent) {
  const logger = new Logger({ source: "middleware" });
  await logger.middleware(request, { logRequestDetails: ["body", "nextUrl"] });

  event.waitUntil(logger.flush());
  return NextResponse.next();
}

Log levels

The log level defines the lowest level of logs sent to Better Stack. Choose one of the following levels (from lowest to highest):

  • debug is the default setting. It means that you send all logs to Better Stack.
  • info
  • warn
  • error means that you only send the highest-level logs to Better Stack.
  • off means that you don't send any logs to Better Stack.

For example, to send all logs except for debug logs to Better Stack:

Set environment variable to change log level
export NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL=info

Web Vitals

To send Web Vitals to Better Stack, add the BetterStackWebVitals component to the app/layout.tsx file:

Send Web Vitals to Better Stack in Next.js v14
import { BetterStackWebVitals } from '@logtail/next/webVitals';

export default function RootLayout() {
  return (
    <html>
      ...
      <BetterStackWebVitals />
      <div>...</div>
    </html>
  );
}
import { BetterStackWebVitals } from '@logtail/next';

export default function RootLayout() {
  return (
    <html>
      ...
      <BetterStackWebVitals />
      <div>...</div>
    </html>
  );
}

Capture errors

To capture routing errors, use the error handling mechanism of Next.js:

  1. Go to the app folder.
  2. Create an error.tsx file.
  3. Inside your component function, add useLogger to send the error to Better Stack. For example:
Send errors to Better Stack in Next.js v14
"use client";

import { useLogger } from "@logtail/next/hooks";
import { LogLevel } from "@logtail/next";
import { usePathname } from "next/navigation";

export default function ErrorPage({
  error,
}: {
  error: Error & { digest?: string };
}) {
  const pathname = usePathname()
  const log = useLogger({ source: "error.tsx" });
  let status =  error.message == 'Invalid URL' ? 404 : 500;

  log.logHttpRequest(
    LogLevel.error,
    error.message,
    {
      host: window.location.href,
      path: pathname,
      statusCode: status,
    },
    {
      error: error.name,
      cause: error.cause,
      stack: error.stack,
      digest: error.digest,
    },
  );

  return (
    <div className="p-8">
      Ops! An Error has occurred:{" "}
      <p className="text-red-400 px-8 py-2 text-lg">`{error.message}`</p>
      <div className="w-1/3 mt-8">
        <NavTable />
      </div>
    </div>
  );
}
"use client";

import { useLogger, LogLevel } from "@logtail/next";
import { usePathname } from "next/navigation";

export default function ErrorPage({
  error,
}: {
  error: Error & { digest?: string };
}) {
  const pathname = usePathname()
  const log = useLogger({ source: "error.tsx" });
  let status =  error.message == 'Invalid URL' ? 404 : 500;

  log.logHttpRequest(
    LogLevel.error,
    error.message,
    {
      host: window.location.href,
      path: pathname,
      statusCode: status,
    },
    {
      error: error.name,
      cause: error.cause,
      stack: error.stack,
      digest: error.digest,
    },
  );

  return (
    <div className="p-8">
      Ops! An Error has occurred:{" "}
      <p className="text-red-400 px-8 py-2 text-lg">`{error.message}`</p>
      <div className="w-1/3 mt-8">
        <NavTable />
      </div>
    </div>
  );
}

How can I extend the logger?

You can use log.with to create an intermediate logger, for example:

Extend logger with additional context
const logger = userLogger().with({ userId: 42 });
logger.info('Hi'); // will ingest { ..., "message": "Hi", "fields" { "userId": 42 }}

Send traces and metrics using OpenTelemetry

Send traces, metrics, and logs from your Next.js app to Better Stack using OpenTelemetry with the @vercel/otel package.

Install the packages:

Install OpenTelemetry packages
npm install @vercel/otel @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/exporter-logs-otlp-http @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics @opentelemetry/sdk-logs

Create an instrumentation.ts file in the root of your project, or in the src folder if you use one:

instrumentation.ts
import { registerOTel } from '@vercel/otel';

export async function register() {
  // OTLP exporters run in the Node.js runtime only
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;

  const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http');
  const { OTLPMetricExporter } = await import('@opentelemetry/exporter-metrics-otlp-http');
  const { OTLPLogExporter } = await import('@opentelemetry/exporter-logs-otlp-http');
  const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-node');
  const { PeriodicExportingMetricReader } = await import('@opentelemetry/sdk-metrics');
  const { BatchLogRecordProcessor } = await import('@opentelemetry/sdk-logs');

  const headers = { Authorization: 'Bearer $SOURCE_TOKEN' };

  registerOTel({
    serviceName: 'your-nextjs-app',
    spanProcessors: [
      new BatchSpanProcessor(
        new OTLPTraceExporter({ url: 'https://$INGESTING_HOST/v1/traces', headers })
      ),
    ],
    metricReader: new PeriodicExportingMetricReader({
      exporter: new OTLPMetricExporter({ url: 'https://$INGESTING_HOST/v1/metrics', headers }),
    }),
    logRecordProcessor: new BatchLogRecordProcessor(
      new OTLPLogExporter({ url: 'https://$INGESTING_HOST/v1/logs', headers })
    ),
  });
}


Next.js runs register on server start. You should see your data in Better Stack → Live tail.

For more instrumentation options, see our OpenTelemetry guide.

Upgrade to the App Router

@logtail/next switched to support the App Router starting with version 0.2.0. If you are upgrading a Pages Router app with @logtail/next v0.1.x to the App Router, you will need to make the following changes:

  • Upgrade @logtail/next to version 0.2.0 or higher
  • Make sure that exported variables has NEXT_PUBLIC_ prefix, e.g: NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN
  • Use useLogger hook in client components instead of log prop
  • For server side components, you will need to create an instance of Logger and flush the logs before component returns.
  • For web-vitals, remove reportWebVitals() and instead add the BetterStackWebVitals component to your layout.