Wiring Up a Next.js Self-Hosted Application to Honeycomb

Wiring Up a Next.js Self-Hosted Application to Honeycomb

12 Min. Read

Are you attempting to connect Honeycomb to a standalone (not hosted with Vercel) Next.js application? Most of the Next.js OpenTelemetry samples in the wild show how to connect Next.js to Vercel’s observability solution when hosting on their platform. But what if you’re hosting your own standalone Next.js server on Node.js?

This blog post will get you started ingesting your Next.js application’s telemetry into Honeycomb. I’ll show you the configuration steps, how to view your traces in Honeycomb, and even how to explore your frontend React telemetry with our Frontend Observability Web Launchpad.

For this example, we assume you’re using version 14 or above of Next.js with TypeScript, and that you’ve decided to use the app folder feature.

What makes Next.js challenging to instrument?

Over the past few years, React has moved into the world of server-driven frameworks with platforms like Next.js, which provides server-side rendering, server components, static site generation, and server actions, to name a few features.

Your Next.js application runs both in the browser, and on a Next.js server:

Rox:  My alt text for the above image:  A Next.js application is instrumented in two places. First, the client-side React application that is downloaded to the browser is instrumented with a client-side React component using OpenTelemetry’s Browser Telemetry support, using Honeycomb’s wrapper SDK. This telemetry can include page and resource load events, user interactivity, calls to client-side frameworks, fetch, and XMLHttpRequest calls. The server side of Next.js, hosted within Node.js in a standalone case, is instrumented with OpenTelemetry for Node.js in much the same way as any other Node.js application such as Express. OpenTelemetry can instrument server actions, server-side rendering, and any backend-for-frontend applications such as fetch calls to another service. 

Next.js blurs the line between frontend and backend

Next.js and similar server-driven component frameworks attempt to make it easier to shift the location of your services and components. Some things that open up for you:

  • Where should my component run? Would it make more sense to render it on the server and ship it to the client, or does it need access to state, hooks, and other features Next.js can only supply via React in a browser? 
  • Should I use Next.js Server Actions and Mutations instead of pointing my frontend calls to other endpoints in my enterprise, treating Next.js as a backend-for-frontend? 
  • Should I pre-generate pages statically at build time for my product catalog, or do I need to incrementally generate them at runtime because my data might change?
  • Do I really need Next.js or do I have an application that mostly does its work in the browser? How would I know where the work is taking place?

Before OpenTelemetry, it was hard to figure out how well each architectural decision would perform. But instrumenting both sides gives you the power of observation, allowing you to create experiments and get them in front of users, via A/B testing and feature flags, and see what might be the better approach using actual user data.


RUM can leave questions unanswered.
Honeycomb for Frontend Observability doesn’t.


Let’s see what we can do!

As an example of what you can do with Next.js, here’s a sample Server Action that loads data directly from a PostgreSQL database using the Node.js pg library:

A sample Next.js action – getLibraryBooks: See it in my Next.js demo repo.

'use server';

import {getPool} from "@/app/utils/pool";

export async function getLibraryBooks() {
    return new Promise(async (resolve, reject) => {
        try {
            const pool = getPool();
            // renaming to camelCase for returned data, ibid... it's a toy
            // app demo, there are much better ways to handle this
            const result = await pool.query(
                `SELECT isbn, name, description, publication_date 
                        AS "publicationDate"
             FROM books 
             ORDER BY id ASC`);
            resolve(result.rows);
        } catch (err) {
            reject(err);
        }
    });
}

This makes Next.js a backend service provider as well as a React frontend. A dessert and a floor wax! Yum? 

Let’s instrument!

To achieve observability, there are two places you’ll have to instrument with OpenTelemetry: the client-side React frontend downloaded to your browser, and the server-side Next.js components and services running on Node.js. 

If you don’t properly instrument both sides, you may either see client-side tracing only, server-side tracing only, or disconnected traces in Honeycomb as the two sides would fail to link up properly. Connected traces show you the full picture.

We’ll look at wiring both sides in this post.

Instrumenting the Next.js React client

Let’s start by wiring up the client-side components. With a Next.js application, we instrument the frontend within a React client-side component, mounted at the top level of your application so it doesn’t accidentally get unloaded or re-rendered.

Here is a component that sets up client-side instrumentation, BrowserTelemetry:

// IMPORTANT - this MUST be a client-only component
'use client';


import {HoneycombWebSDK} from "@honeycombio/opentelemetry-web";
import {getWebAutoInstrumentations} 
  from "@opentelemetry/auto-instrumentations-web";

export function BrowserTelemetry() {
   // I defined the ingest API key in the Next.js .env file
   const apiKey = process.env.NEXT_PUBLIC_HONEYCOMB_API_KEY;

   if (!apiKey) {
     return null;
   }

   try {
     const webSDK = new HoneycombWebSDK({
       serviceName: 'react-frontend',
       apiKey: apiKey,
       // .. additional settings here ..
     });
       
     webSDK.start();
  } catch (e) {
      // report any errors, but do not blow up frontend
      // when OpenTelemetry cannot configure itself
      console.error(e);
  }
  
  // this component doesn't render anything
  return null;
}

If you want to avoid exposing your ingest key (any NEXT_PUBLIC_ variables are exposed to the browser itself inside of scripts), you can set up an OpenTelemetry Collector.

Now, we need to mount the component in the React client-side. What does Next.js provide us that we can leverage?

Mount the client-side instrumentation component

The most direct place to mount browser telemetry in a Next.js application is within your application’s top-level layout component because it will be loaded immediately and should stay loaded for the life of your users’ interactions.

Assuming you use the app folder, this layout component is located in app/layout.tsx, so go ahead and mount the BrowserTelemetry client instrumentation component in this file:

export default function RootLayout({
  children,
}: Readonly<{
  children: ReactNode;
}>) {
  return (
    <html lang="en">
      {/* Mount your client-side telemetry component here */}
      <BrowserTelemetry />
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >

      <h1 className="py-3 text-5xl font-bold font-sans">
        I am a title!
      </h1>
        <Menu />
        {children}
      </body>
    </html>
  );
}

Avoid placing any telemetry configuration component within a route-driven, nested Next.js layout. This could have unintended consequences, such as writing errors to the console. 

Avoiding spurious errors in the Next.js server log

If you build and deploy your application now, you’ll find that it starts sending browser-side React telemetry (refer to my earlier COS troubleshooting guide if you’re not sure why we have both a POST + Preflight and OPTIONS call invocation for a React frontend).

Next.js OTel sample - screenshot.

However, you’ll also likely see some odd errors emitted from the Next.js server logs:

ReferenceError: document is not defined
    at b (/Users/kenrimple/projects/hacking/nextjs-demo/.next/server/chunks/ssr/node_modules_235c3a._.js:15736:5)
...
[root of the server]__1c7666._.js:141:25)

Why is the Next.js server attempting to access the webpage’s document? This happens when Next.js tries to pre-render your client components on the server, before hydrating them and sending them to the page. This rendering even includes your observability component. 

The Node.js server can’t access browser elements like document and window, and access to a DOM, events, and other browser-y things will be undefined. Splat!

Skip browser instrumentation during pre-rendering

There is an easy fix! Check whether the React client component is in the middle of a pre-render operation on the server side. The procedure:

  1. Make sure your component is mounted as a client component with use client at the very top of your script (we’ve done that already).
  2. Evaluate whether your component is running in an environment with a window object visible to it (i.e., a real window, a browser).
  3. If it is not being rendered in a browser, scram! Get out of the rendering process early. Otherwise, proceed as normal and wire up telemetry.

An updated BrowserTelemetry component:

// as before - send it to the browser
'use client';


import {HoneycombWebSDK} from "@honeycombio/opentelemetry-web";
import {getWebAutoInstrumentations} from "@opentelemetry/auto-instrumentations-web";

const apiKey = process.env.NEXT_PUBLIC_HONEYCOMB_API_KEY;

export function BrowserTelemetry() {

   // New: only run on server, not on client. 
   // Get out if it tries to SSR this. Also exit if
   // the apiKey isn't present
    if (typeof window === 'undefined' || !apiKey) {
       return null;
    }
   
    try {
      // our open source SDK provides sensible instrumentation
      // choices by default, including adding core web vitals
      // and enhanced web attributes in browser-generated spans
           const webSDK = new HoneycombWebSDK({
             serviceName: 'react-frontend',
             apiKey,
			.. settings here ..
           });
           webSDK.start();
      } catch (e) {
        // report any errors, but do not do anything that
        // would crash the user interface (like re-throwing
        // an exception) if the start() method throws one,
        // or your telemetry problem could make the application
        // unusable.
        console.error(e);
      }

  // this component doesn't render anything
  return null;
}

Wiring Honeycomb to the Next.js server side

Now that we’ve gotten the client instrumentation working, we can wire the self-hosted Next.js server’s Node.js runtime. This is the same as instrumenting any other Node.js service with OpenTelemetry. 

However, we don’t have an obvious server-side entry point to do the wiring. 

Use instrumentation.ts for server instrumentation

If you place an instrumentation.ts file in the top level code directory, Next.js automatically loads it and calls a function named register to load your instrumentation. We’ll use a standard collector configuration for Node:

Place this in src/instrumentation.ts:

import {OTLPTraceExporter} from "@opentelemetry/exporter-trace-otlp-http";
import {getNodeAutoInstrumentations} 
    from "@opentelemetry/auto-instrumentations-node";

const apiKey = process.env.NEXT_PUBLIC_HONEYCOMB_API_KEY;

// use standard OpenTelemetry instrumentation to your Node.js server
const sdk = new opentelemetry.NodeSDK({
  url: `https://api.honeycomb.io/v1/traces`,
  headers: { 'X-Honeycomb-Team': apiKey },
  serviceName: 'nextjs-serverside',
  instrumentations: [getNodeAutoInstrumentations()]
});

try {
   sdk.start();
} catch (e) {
   console.log('Failure in instrumentation');
   console.error(e);
   return;
}

// clean up on our way out and properly flush the remaining telemetry
// if possible.
if (process.env.NEXT_RUNTIME === "nodejs") {
  process.on('SIGTERM', () => {
    sdk
        .shutdown()
        .finally(() => process.exit(0));
  });
}

Note: Since you are self-hosting the application, do not use Next.js’s registerOTel method, which wires you to Vercel’s telemetry solutions in their cloud.

See your data in Honeycomb!

Now that we’ve properly wired Honeycomb in both the client and server, let’s take a look at how to view our telemetry data from Honeycomb. 

I’ve written a sample repository which demonstrates a fully instrumented Next.js application. It includes telemetry for both the client-side React services and the Next.js server operations. See the README.md file for details. The output below uses this application.

A React component calling a Next.js server-side action

I created a trace showing how a React client-side component interacts with a Next.js server-side action by logging into Honeycomb, choosing the proper team, selecting the environment that I defined (which contains my Honeycomb ingest key), and clicking on the Query icon. I chose the nextjs-client dataset. My query settings were: 

  • Visualize: COUNT
  • WHERE:  is_root, name = HTTP POST, page.route = /books

I then clicked Run Query. To see traces, I clicked the tab below the graph labeled “Traces.” Then, I clicked on the trace icon on the left-hand side (also could have clicked on the trace id link on the right) to show my trace.

In the selected trace, my client-side React component uses Next.js to call a server-side Next.js action. The client-side React service name is nextjs-client and our Next.js server actions run on Node.js. I’ve instrumented the server using nextjs-server as the service name:

As you can see, we are now instrumenting both the React frontend and the Next.js backend, which:

  1. The React client initiates a fetch POST call (Note: server actions in Next.js are triggered via POST methods)
  2. Next.js runs the server action on the /books route
  3. The ServerAction requests a Postgres database pool connection
  4. The ServerAction executes the SQL statement to query the books table
  5. The data is returned to the React client as serialized JSON data from the query results

The Honeycomb Frontend Observability Web Launchpad view

If you have access to the Honeycomb Web Launchpad, the frontend dataset’s landing page is the Web Launchpad. It provides key telemetry about your web frontend application, including Core Web Vitals, information about fetched assets, long running endpoints, a breakdown of events by type, and much more. All of this information leads to queries in Honeycomb, and can be shared with other developers. 

The React dataset (reactjs-client) in the Honeycomb Web Launchpad.

Resources

Have questions about wiring up Next.js, React, Angular, Vue, or any other Honeycomb for Frontend Observability concerns with Honeycomb? You can join Pollinators, Honeycomb’s Slack community, or you can request office hours. Choose Ken—that’s me! I’m happy to help you get going.

Don’t forget to share!
Ken Rimple

Ken Rimple

Senior Developer Relations Advocate

Ken Rimple, (Senior Developer Relations Advocate, he/him), is a software engineer with more than 35 years of experience. He’s developed on databases, application servers, front-end JS and TypeScript frameworks, and is currently focused on front-end applications and stacks with React, Angular, and other APIs and frameworks. He lives in the Philadelphia, Pennsylvania area of the US with his wife, four mostly adult children and three dogs.

Related posts