Skip to content

Integrating Next.js with New Relic

Integrating Next.js with New Relic

Integrating Next.js with New Relic

When it comes to application monitoring, New Relic is genuinely one of the veterans in the market. Founded in 2008, it offers a large set of tools designed for developers to help them analyze application performance, troubleshoot problems, and gain insight into user behavior through real-time analytics.

We needed to integrate New Relic's application monitoring with a modern Next.js app built on top of the app router. This blog post will explain your options if you want to do the same in your project. Please keep in mind that depending on when you read this blog post, there may be some advances in how New Relic works with Next.js. You can always refer to the official New Relic website for up-to-date documentation.

The approach is also different, depending on whether you use Vercel's managed Next.js hosting (or any other provider that is lambda-based) or self-host Next.js on an alternative provider such as Fly.io. We'll start with the typical steps you need to do for both ways of deploying; then we'll continue with the self-hosted option, which uses a complete set of features from New Relic, and finish with Vercel where we recommend the OpenTelemetry integration for now.

Common Prerequisite Steps

First, let's install the necessary packages. These were built by New Relic and are needed whether you self-host your Next.js app or host it on Vercel.

npm install newrelic @newrelic/next
npm install -D @types/newrelic

The next thing you'll want to do is define your app name. The name will be different depending on the environment the app is running in, such as a local environment, staging, or production. This will ensure that the data you send to New Relic is always tagged with a proper app name and will not be mixed between environments.

An environment-changing parameter, such as the app name, should always be included in an environment variable. In your local environment, that would be in .env.local, while staging/production or any other hosted environments would be configured in the provider.

NEW_RELIC_APP_NAME='My App - Staging'

To get full coverage with New Relic, in Next.js, we need to cover the parts running on the client (in the browser) and on the server. The server-running part is different on Vercel's managed hosting, where all of the logic is executed within lambdas, and self-hosting variants, where the server logic is executed on a Node.js server.

The client part is configured by going to the New Relic dashboard and clicking "Add Data" / "Browser Monitoring". Choose "place a JavaScript snippet in frontend code," name it the same name you used in the environment variable above, click "Continue" to the last step, and copy the part between the script tags:

<script type="text/javascript">
	// big chunk of JavaScript that you need to copy
</script>

Then paste it to a variable inside any module of your code. For example, this is how we did it:

// app/components/new-relic-scripts-per-env.ts

export const DEV_SCRIPT=`script contents`;
export const STAGING_SCRIPT=`script contents`;
export const PRODUCTION_SCRIPT=`script contents`;

Now, create a component that will use this script and put it to Next's Script component. Then you can use the component in your root layout:

// app/components/new-relic-script.tsx
import Script from "next/script";
import * as React from "react";
import { IS_PRODUCTION, IS_STAGING } from "@/utilities/environment";
import {
  DEV_SCRIPT,
  PRODUCTION_SCRIPT,
  STAGING_SCRIPT,
} from "./new-relic-scripts-per-env";

function getScriptContent() {
  if (IS_PRODUCTION) {
    return PRODUCTION_SCRIPT;
  }

  if (IS_STAGING) {
    return STAGING_SCRIPT;
  }

  return DEV_SCRIPT;
}

export default function NewRelicScript() {
  const scriptContent = getScriptContent();

  return (
    <Script
      id="nr-browser-agent"
      strategy="beforeInteractive"
    >
      {scriptContent}
    </Script>
  );
}

There are several things to note here. First, we need to set a unique ID for the Script component for Next.js to track and optimize the script. We also need to set the strategy to beforeInteractive so that the New Relic script is added to the document’s head element. Finally, we use some environment helper variables. IS_PRODUCTION and IS_STAGING are just helper variables that read the environment variables to determine if we are in a local environment, staging, or production.

Now you can create an app router error page and report any uncaught errors to New Relic:

// app/error.tsx
"use client";

import { useEffect } from "react";

export default function ErrorPage({
  error,
}: {
  error: Error & { digest?: string };
}) {
  useEffect(() => {
    if (window.newrelic) {
      window.newrelic.noticeError(error);
    } else {
      console.error("Cannot report error to New Relic", error);
    }
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
    </div>
  );
}

With this in place, the client part is covered, and you should start seeing data in the New Relic dashboard under "All Entities" / "Browser Applications."

Let's now focus on the server part, with both a self-hosted option and Vercel.

Self-Hosting Next.js

If you self-host Next.js, It will typically run through a Node server (containerized or not), meaning you can fully utilize New Relic's APM agent.

Start by adding this to the Next.js config (next.config.js) to mark the newrelic package as external, otherwise you would not be able to import it.

{
  ...
  experimental: {
    ...
    // Without this setting, the Next.js compilation step will routinely
    // try to import files such as `LICENSE` from the `newrelic` module.
    // Note that this property will be renamed in the future: https://github.com/vercel/next.js/pull/65421
    serverComponentsExternalPackages: ["newrelic"],
  },
}

The next step that New Relic recommends is to copy the newrelic.js file from the newrelic library to the root of the folder. However, we want to keep our project root clean, and we'll store the file in o11y/newrelic.js (β€œo11y” is an abbreviation for observability). This will also demonstrate the configuration needed to make this possible.

This file, by default, will look something like this.

"use strict";

/**
 * New Relic agent configuration.
 *
 * See lib/config/default.js in the agent distribution for a more complete
 * description of configuration variables and their potential values.
 */
exports.config = {
  /**
   * Array of application names.
   */
  app_name: [process.env.NEW_RELIC_APP_NAME],
  /**
   * Your New Relic license key.
   */
  license_key: process.env.NEW_RELIC_LICENSE_KEY,
  agent_enabled: true,
  logging: {
    /**
     * Level at which to log. 'trace' is most useful to New Relic when diagnosing
     * issues with the agent, 'info' and higher will impose the least overhead on
     * production applications.
     */
    level: "info",
  },
  /**
   * When true, all request headers except for those listed in attributes.exclude
   * will be captured for all traces, unless otherwise specified in a destination's
   * attributes include/exclude lists.
   */
  allow_all_headers: true,
  attributes: {
    /**
     * Prefix of attributes to exclude from all destinations. Allows * as wildcard
     * at the end.
     *
     * NOTE: If excluding headers, they must be in camelCase form to be filtered.
     *
     * @name NEW_RELIC_ATTRIBUTES_EXCLUDE
     */
    exclude: [
      "request.headers.cookie",
      "request.headers.authorization",
      "request.headers.proxyAuthorization",
      "request.headers.setCookie*",
      "request.headers.x*",
      "response.headers.cookie",
      "response.headers.authorization",
      "response.headers.proxyAuthorization",
      "response.headers.setCookie*",
      "response.headers.x*",
    ],
  },
};

As you can see, some environment variables are involved, such as NEW_RELIC_APP_NAME and NEW_RELIC_LICENSE_KEY. These environment variables will be needed when the New Relic agent starts up. The trick here is that the agent doesn't start up with your app but earlier by preloading the library through another environment variable, NODE_OPTIONS.

On the server, that would be done by modifying the start script to this:

{
  "scripts": {
    ...
    "start": "NODE_OPTIONS='--loader newrelic/esm-loader.mjs -r @newrelic/next --no-warnings' next start",
  }
}

This ensures the agent is preloaded before starting the app, and the rest of the environment variables (such as NEW_RELIC_APP_NAME and NEW_RELIC_LICENSE_KEY) should already exist. Making sure this works in local development is not that easy due to different stages of loading environment variables. The environment variables from .env.local are loaded by the framework, which is too late for the New Relic agent (which is preloaded by the Node process). However, it is possible with a trick.

First, add the following new environment variables to .env.local:

NEW_RELIC_LICENSE_KEY=# take from 1Password
NEW_RELIC_HOME=o11y
# NODE_OPTIONS is needed only for local development
NODE_OPTIONS='--loader newrelic/esm-loader.mjs -r @newrelic/next --no-warnings'

(We have not mentioned NEW_RELIC_HOME so far, but the New Relic agent implicitly uses it to locate the newrelic.js file. We need to specify that since we moved it to the o11y folder.)

Next, install the nodenv package.

npm install -D node-env-run

The nodenv package provides a cross-platform way of preloading environment variables before we start the Node process. If we provide it with the .env.local file, this means all of the environment variables from that file will be available to the New Relic agent!

Change the scripts in package.json:

{
  "scripts": {
    "dev": "nodenv -E .env.local --exec \"next dev --turbo -H apollo.knopmanmarks.localhost -p 3000\"",
    "start": "NODE_OPTIONS='--loader newrelic/esm-loader.mjs -r @newrelic/next --no-warnings' next start",
  }
}

And that's it. If you now start the local server using the dev command, nodenv will make environment variables available to the Node process. The Node process will preload the New Relic agent (because we have NODE_OPTIONS in .local.env), the agent will read all the environment variables needed by newrelic.js, and everything will work as expected.

The first thing you might notice is the creation of the newrelic_agent.log. This is just a log file by the agent which you can use to troubleshoot if the agent is not working correctly. You can safely add this file to .gitignore.

From this point on, the server-side part of the app can utilize New Relic's API (such as noticeError and recordLogEvent) and the agent will automatically be recording transactions made through the server. The data should be visible in the New Relic dashboard, under "All Entities" / "Services - APM".

This is how you would log an error in your server action, for example:

import newrelic from "newrelic";

export async function createUserAction(...) {
  try {
    // create user logic
  } catch (err) {
    newrelic.noticeError(err);
  }
}

The full source of this type of integration is available on StackBlitz.

Using the New Relic APM agent on Vercel is currently not possible, as the server part of Next.js runs within lambdas, and not within a typical Node server as in the self-hosted option. In the future, this may be possible. At the moment, the only way to integrate Vercel and New Relic is by using the Vercel OpenTelemetry collector, which is only available for some plans.

The first step is to enable the integration in Vercel's dashboard and then install the following packages:

npm install @vercel/otel @opentelemetry/api

Create an instrumentation.ts file in the root of your project and add the following code:

import { registerOTel } from "@vercel/otel";
import { NEW_RELIC_APP_NAME } from "@/utilities/environment";

registerOTel({ serviceName: NEW_RELIC_APP_NAME });

In the the Next.js config (next.config.js), add:

{
  ...
  experimental: {
    instrumentationHook:
      !!process.env.VERCEL_ENV && process.env.VERCEL_ENV !== "development", // Only use instrumentation if deployed to Vercel
  },
}  

The OpenTelemetry integration does not work locally, hence the above conditional check. With the above configuration, the collected OpenTelemetry data should be visible in the New Relic dashboard under "All Entities" / "Services - OpenTelemetry".

Optionally, Logging via New Relic API

When using the Vercel OpenTelemetry integration, there is no easy way to submit custom logs to New Relic. New Relic offers a logging API, however, so to send custom logs from your backend, you could use a logger such as Winston, which has good library support for custom transports, such as the one for New Relic.

Install the following packages:

npm install winston winston-nr

Then create a logger.ts file with the following code:

import {
  IS_VERCEL,
  LOG_LEVEL,
  NEW_RELIC_APP_NAME,
  NEW_RELIC_LICENSE_KEY,
} from "./environment";
import { headers } from "next/headers";
import winston from "winston";
import { NewRelicTransport } from "winston-nr";

const newRelicTransport = new NewRelicTransport({
  apiUrl: "https://log-api.newrelic.com/log/v1",
  apiKey: NEW_RELIC_LICENSE_KEY,
  compression: true,
  retries: 3,
  batchSize: 10,
  batchTimeout: 5000,
  format: winston.format.json(),
});

export const serverLogger = winston.createLogger({
  level: LOG_LEVEL,
  defaultMeta: { serviceName: NEW_RELIC_APP_NAME },
  transports: [newRelicTransport],
});

NewRelicTransport utilizes the New Relic logging API to send application logs. Now you can use the serverLogger in your backend code, from server components to server actions.

Conclusion

As you can see, various options exist for integrating New Relic with the modern, app-router-based Next.js. Sadly, at the time of writing, no one-size-fits-all solution works equally for all hosting options, as Vercel's managed Next.js platform is still relatively young, and so is the app router in Next.js.

If you liked this blog post, feel free to browse our other Next.js blog posts for more tips and tricks in other areas of the Next.js ecosystem.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co