Grafana Cloud

Instrument Next.js applications

Follow these steps to get started quickly with the Faro Web SDK and Grafana Cloud Frontend Observability:

  1. Create a Frontend Observability application
  2. Set environment variables to configure the instrumentation
  3. Install the Faro Web SDK
  4. Instrument the Next.js backend
  5. Enable backend correlation
  6. Verify backend correlation is working
  7. Observe the application in Frontend Observability
  8. Upload source maps (optional)

The Faro Web SDK is a highly configurable open source real user monitoring (RUM) library built on OpenTelemetry and integrating seamlessly with Grafana Cloud and Grafana Frontend Observability.

Next.js is a popular hybrid framework that you can instrument for Real User Monitoring with Grafana Cloud. By monitoring your Next.js app with Frontend Observability, you can achieve the following:

  • Monitor the performance of your site: collect performance KPIs such as Web Vitals automatically
  • Error capturing: leverage the automatic error capturing on the browser side and get line-level insights on error
  • End-to-end observability: automatically connect RUM sessions with APM and vice-versa for maximum insights on performance and root-causes

Example code for this guide

You can find an example application that’s already fully customized at https://github.com/grafana/faro-nextjs-example.

Create a Frontend Observability application

To observe Real User Monitoring (RUM) data in Grafana Cloud Frontend Observability, create an application in the Grafana Cloud Frontend Observability plugin:

  1. Sign in to Grafana Cloud, register for a Free Grafana Cloud account if required, to get to the Grafana Cloud Portal page.

    If the account has access to multiple Grafana Cloud Organizations, select an organization from the top left organization drop-down.

    If the organization has access to multiple Grafana Cloud Stacks, navigate to a stack from the left side bar or the main Stacks list.

  2. With a stack selected, or in the single stack scenario, below Manage your Grafana Cloud Stack, click Launch in the Grafana section:

    The Launch button that opens Grafana Cloud

  3. Use the left navigation to expand Frontend and select Frontend Apps.

    If this is the first time you access Frontend Observability it displays the landing page. Click Start observing on the landing page to navigate to the overview page.

  4. From the overview page, click Create new to create a Frontend Observability application to hold RUM data for your web application.

    To create an application requires the following information:

    • App Name: Give your app a meaningful name
    • CORS Allowed Origin: Domains allowed to access your Frontend Observability application, for local development set the value to localhost
    • Default attributes: Any attributes you’d like added to all signals, if unknown leave empty
  5. From the Web SDK Configuration tab, copy the url value. You’ll use this in the next step when configuring environment variables.

Set environment variables to configure the instrumentation

Provide the necessary environment variables to configure your application. Use the url value from the Frontend Observability application you created in the previous step.

The following table describes each environment variable:

VariableDescription
NEXT_PUBLIC_FARO_URLThe URL of your Faro instance from Frontend Observability > Web SDK Configuration
NEXT_PUBLIC_FARO_APP_NAMEThe name of your app, should be different for your backend and frontend
NEXT_PUBLIC_FARO_APP_NAMESPACEThe namespace of your app, should optimally be the same as the namespace of your backend
OTEL_EXPORTER_OTLP_ENDPOINTThe OTLP endpoint URL, example assumes the collector runs on the same machine
OTEL_EXPORTER_OTLP_PROTOCOLThe OTLP protocol, set to http/protobuf
OTEL_SERVICE_NAMEThe backend service name
OTEL_RESOURCE_ATTRIBUTESResource attributes including service.namespace, set to the same value as the frontend namespace to enable correlation

Example configuration:

NEXT_PUBLIC_FARO_URL=my-url
NEXT_PUBLIC_FARO_APP_NAME=next-frontend
NEXT_PUBLIC_FARO_APP_NAMESPACE=nextjs-example

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_SERVICE_NAME=next-backend
OTEL_RESOURCE_ATTRIBUTES=service.namespace=nextjs-example

Install the Faro Web SDK

First, add the Faro Web SDK to your project. Install the Faro Web SDK package by running the following command for NPM:

sh
npm i @grafana/faro-web-sdk

Or the following command Yarn:

sh
yarn add @grafana/faro-web-sdk

Create a component in components/frontend-observability.tsx so you can inject the Web SDK into your Next.js frontend:

ts
'use client';

import { faro, getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';

export default function FrontendObservability() {
  // skip if already initialized
  if (faro.api) {
    return null;
  }

  try {
    const faro = initializeFaro({
      url: process.env.NEXT_PUBLIC_FARO_URL,
      app: {
        name: process.env.NEXT_PUBLIC_FARO_APP_NAME || 'unknown_service:webjs',
        namespace: process.env.NEXT_PUBLIC_FARO_APP_NAMESPACE || undefined,
        version: process.env.VERCEL_DEPLOYMENT_ID || '1.0.0',
        environment: process.env.NEXT_PUBLIC_VERCEL_ENV || 'development',
      },

      instrumentations: [
        // Mandatory, omits default instrumentations otherwise.
        ...getWebInstrumentations(),

        // Tracing package to get end-to-end visibility for HTTP requests.
        new TracingInstrumentation(),
      ],
    });
  } catch (e) {
    return null;
  }
  return null;
}

The Faro Web SDK captures data about your application’s health and performance and exports it to a managed Grafana Cloud endpoint.

Open source users can use the Grafana Alloy as their data collector. See the Faro receiver with Grafana Alloy documentation to learn more.

Instrument the Next.js backend

To get the full picture, you should also instrument the backend parts of your Next.js app.

This allows you to correlate with backend activity from the frontend and create a better understanding and broader set of signals when investigating an issue.

Install the required packages:

Bash
npm install @vercel/otel @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation

Create a new file instrumentation.ts in the root of your project. This file contains the configuration for the OpenTelemetry instrumentation for the Node.js backend of your app.

It contains a few defaults and a custom span processor to reduce the cardinality of span names.

ts
import { Context } from '@opentelemetry/api';
import { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-node';
import { registerOTel } from '@vercel/otel';

/**
 * Span processor to reduce cardinality of span names.
 *
 * Customize with care!
 */
class SpanNameProcessor implements SpanProcessor {
  forceFlush(): Promise<void> {
    return Promise.resolve();
  }
  onStart(span: Span, parentContext: Context): void {
    if (span.name.startsWith('GET /_next/static')) {
      span.updateName('GET /_next/static');
    } else if (span.name.startsWith('GET /_next/data')) {
      span.updateName('GET /_next/data');
    }
  }
  onEnd(span: ReadableSpan): void {}
  shutdown(): Promise<void> {
    return Promise.resolve();
  }
}

export function register() {
  registerOTel({
    serviceName: process.env.OTEL_SERVICE_NAME || 'unknown_service:node',
    spanProcessors: ['auto', new SpanNameProcessor()],
  });
}

Note

For even more insights, take a look at @vercel/otel to learn how you can include more instrumentation, such as automatic tracing for databases and other important software.

Enable backend correlation

Create a middleware.ts file in the root of your app to enable correlation between frontend and backend telemetry. This middleware extracts the trace context from the active span and injects it into the response headers through the server-timing header, allowing Frontend Observability to match client-side performance KPIs with the exact backend requests.

typescript
import { NextRequest, NextResponse } from 'next/server';
import { trace } from '@opentelemetry/api';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const current = trace.getActiveSpan();

  // set server-timing header with traceparent
  if (current) {
    response.headers.set(
      'server-timing',
      `traceparent;desc="00-${current.spanContext().traceId}-${current.spanContext().spanId}-01"`
    );
  }
  return response;
}

Enable Client service mapping in Application Observability

In your Grafana Cloud stack, enable client services. This ensures that frontend services such as your Next.js app are included in the service graph.

Verify backend correlation is working

After implementing the middleware and backend instrumentation, verify that frontend and backend telemetry are properly correlated. A successful end-to-end trace contains both frontend spans (browser activity) and backend spans (server activity) linked by the same trace ID.

Check the server-timing header

  1. Open your Next.js application in a browser.

  2. Open the browser’s Developer Tools (F12 or right-click > Inspect).

  3. Navigate to the Network tab.

  4. Reload the page or navigate to trigger a request.

  5. Select any request to your Next.js application.

  6. In the Headers section, look for Response Headers.

  7. Verify that a server-timing header is present and contains a traceparent value with a trace ID.

    Example:

    server-timing: traceparent;desc="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"

    The trace ID in this example is 4bf92f3577b34da6a3ce929d0e0e4736.

If the server-timing header is missing or doesn’t contain traceparent, verify that:

  • The middleware.ts file is in the root of your project
  • OpenTelemetry is properly initialized in instrumentation.ts
  • Your Next.js server has restarted after adding the middleware

Verify end-to-end traces in Grafana Cloud

  1. In Grafana Cloud, navigate to Explore.

  2. Select Tempo as the data source (or another appropriate observability data source).

  3. Search for recent traces from your application.

  4. Open a trace and verify it contains both:

    • Frontend spans: Client-side activity such as fetch requests, navigation, or resource loading
    • Backend spans: Server-side activity such as GET /, GET /api/*, or database queries

    Both span types should share the same trace ID, confirming end-to-end correlation.

If traces only contain backend spans or only frontend spans:

  • Verify the TracingInstrumentation is included in the Faro SDK configuration
  • Check that NEXT_PUBLIC_FARO_APP_NAMESPACE matches service.namespace in OTEL_RESOURCE_ATTRIBUTES
  • Confirm that client services are enabled in Application Observability settings

Observe the application in Frontend Observability

Run your web application and make some requests to instrumented pages to send data to Grafana Cloud.

Learn how to explore your application in Frontend Observability with the documentation:

Upload source maps

Uploading source maps is optional but strongly recommended for production applications.

Source maps connect minified production code to original source files, making error stack traces readable in Frontend Observability. Without source maps, error messages and stack traces show minified code positions, making debugging difficult. If your application doesn’t minify code or you’re only testing in development, you can skip source map uploads.

Next.js applications use Turbopack for bundling, which requires a different approach for source map uploads compared to traditional bundlers. For Next.js applications, use the Faro CLI to upload source maps to Grafana Cloud.

Note

If your Next.js application uses webpack instead of Turbopack and generates both client and server code, you can use the Faro webpack plugin with the nextjs: true option. This adds _next/ to the beginning of paths for proper stack trace matching.

Configure source map uploads with the Faro CLI

The Faro CLI uploads source maps separately from your build process, making it ideal for Next.js applications and CI/CD pipelines.

Install the Faro CLI as a development dependency:

sh
npm install --save-dev @grafana/faro-cli

Upload source maps after building your application:

sh
npx faro-cli upload \
  --endpoint "$FARO_SOURCEMAP_API_URL" \
  --app-id "$FARO_APP_ID" \
  --api-key "$FARO_API_KEY" \
  --stack-id "$FARO_STACK_ID" \
  --bundle-id "$BUNDLE_ID" \
  --app-name "next-frontend" \
  --output-path "./.next/static" \
  --verbose

The bundle-id configuration identifies which source maps correspond to which deployed version of your application. This is required for Frontend Observability to match errors with the correct source maps. Refer to bundle ID configuration for details on generating and using bundle IDs.

You can find the source map API endpoint, application ID, and stack ID in Frontend Observability > Settings > Source Maps > Configure source map uploads.

For complete CLI configuration options and advanced usage, refer to Upload source maps with the CLI.

Bundle ID configuration

The bundleId identifies which version of your application’s source maps to use when resolving error stack traces. Frontend Observability uses the bundle ID to match error stack traces with the correct source maps for that deployment.

You must configure the same bundle ID in three places:

  1. In your application code: Set the bundle ID when initializing the Faro Web SDK
  2. In your build artifacts: Inject the bundle ID into JavaScript files
  3. During source map upload: Provide the bundle ID to the CLI

Generate a unique bundle ID for each deployment. Common approaches include:

Using Git commit SHA:

sh
BUNDLE_ID=$(git rev-parse --short HEAD)

Using timestamp:

sh
BUNDLE_ID=$(date +%s)

Using CI/CD build number:

For GitHub Actions:

sh
BUNDLE_ID=$GITHUB_RUN_ID

Configure the bundle ID in your Faro initialization code:

ts
import { initializeFaro } from '@grafana/faro-web-sdk';

initializeFaro({
  url: process.env.NEXT_PUBLIC_FARO_URL,
  app: {
    name: process.env.NEXT_PUBLIC_FARO_APP_NAME,
    version: process.env.VERCEL_DEPLOYMENT_ID || '1.0.0',
    environment: process.env.NEXT_PUBLIC_VERCEL_ENV || 'development',
  },
  bundleId: process.env.NEXT_PUBLIC_BUNDLE_ID,
  // ... other configuration
});

Inject the bundle ID into your built JavaScript files:

sh
npx faro-cli inject-bundle-id \
  --bundle-id "$BUNDLE_ID" \
  --app-name "next-frontend" \
  --files "./.next/static/**/*.js" \
  --verbose

Upload source maps with the same bundle ID:

sh
npx faro-cli upload \
  --endpoint "$FARO_SOURCEMAP_API_URL" \
  --app-id "$FARO_APP_ID" \
  --api-key "$FARO_API_KEY" \
  --stack-id "$FARO_STACK_ID" \
  --bundle-id "$BUNDLE_ID" \
  --app-name "next-frontend" \
  --output-path "./.next/static" \
  --verbose

For complete documentation on source map uploads, refer to Source map uploads.