Next.js Walkthrough
Limitations
⚠️ App Directory support on Vercel is a work-in-progress. Session Replay, Vercel Log Drain and non-Vercel-deployed Error Monitoring are fully operational; however, Vercel's edge runtime is not yet compatible with OpenTelemetry. Vercel's nodejs runtime is instrumented for OpenTelemetry, but we're having trouble ingesting Server-side Rendering (SSR) errors. We expect to solve the nodejs runtime issues first. In the meantime, we're hoping to find an edge runtime-compatible version of OpenTelemetry.
⚠️ Source maps do not work in development mode. Run yarn build && yarn start to test compiled source maps in Highlight.
Installation
# with npm npm install @highlight-run/next @highlight-run/react highlight.run # with yarn yarn add @highlight-run/next @highlight-run/react highlight.run
Environment Configuration (Very optional)
This section is extra opinionated about Next.js constants. It's not for everyone. We like how
zodand TypeScript work together to validateprocess.envinputs... but this is a suggestion. Do your own thing!
.env to add your projectID to NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID.env.local at your project root with variables for your local otlpEndpoint and backendUrl:# .env.local NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID='1jdkoe52' # omit to send data to app.highlight.io NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT='http://localhost:4318' NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL='https://localhost:8082/public'
zod for this example, because it creates a validated, typed CONSTANTS object that plays nicely with TypeScript.// src/app/constants.ts import { z } from 'zod' const stringOrUndefined = z.preprocess( (val) => val || undefined, z.string().optional(), ) // Must assign NEXT_PUBLIC_* env vars to a variable to force Next to inline them const publicEnv = { NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID: process.env.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID, NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT: process.env.NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT, NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL: process.env.NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL, } const CONSTANTS = z .object({ NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID: z.string(), NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT: stringOrUndefined, NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL: stringOrUndefined, }) .parse(publicEnv) export default CONSTANTS
Instrument your API routes
Highlight wrapper function:// src/app/utils/highlight.config.ts: import CONSTANTS from '@/app/constants' import { Highlight } from '@highlight-run/next' if (process.env.NODE_ENV === 'development') { // Highlight's dev instance expects HTTPS. Disable HTTPS errors in development. process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' } export const withHighlight = Highlight({ projectID: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID, otlpEndpoint: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT, backendUrl: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL, })
/pages/api functions with withHighlight:// pages/api/test.ts import { NextApiRequest, NextApiResponse } from 'next' import { withHighlight } from '@/app/utils/highlight.config' import { z } from 'zod' export default withHighlight(function handler( req: NextApiRequest, res: NextApiResponse, ) { const success = z.enum(['true', 'false']).parse(req.query.success) console.info('Here: /api/app-directory-test', { success }) if (success === 'true') { res.send('Success: /api/app-directory-test') } else { throw new Error('Error: /api/app-directory-test') } })
Instrument the server
Next.js comes out of the box instrumented for Open Telemetry. Our example Highlight implementation will use Next's experimental instrumentation feature to configure Open Telemetry on our Next.js server. There are probably other ways to configure Open Telemetry with Next... but this is our favorite.
next-build-id with npm install next-build-id.instrumentationHook. We've also turned on productionBrowserSourceMaps because Highlight is much easier to use with source maps.// next.config.js const nextBuildId = require('next-build-id') /** @type {import('next').NextConfig} */ const nextConfig = { generateBuildId: () => nextBuildId({ dir: __dirname }), experimental: { appDir: true, instrumentationHook: true, }, productionBrowserSourceMaps: true, } module.exports = nextConfig
You may have trouble with a missing __dirname environment variable. This can happen with Next.js middleware. The following example uses a next.config.mjs file instead of the CJS-style next.config.js pattern. It calculates __dirname using native Node.js utility packages.
// next.config.mjs import { dirname } from 'path' import { fileURLToPath } from 'url' import nextBuildId from 'next-build-id' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) /** @type {import('next').NextConfig} */ const nextConfig = { generateBuildId: () => nextBuildId({ dir: __dirname }), experimental: { appDir: true, instrumentationHook: true, }, productionBrowserSourceMaps: true, } export default nextConfig
instrumentation.ts at the root of your project as explained in the instrumentation guide. Call registerHighlight from within the exported register function:// instrumentation.ts import CONSTANTS from '@/app/constants' import { registerHighlight } from '@highlight-run/next' export async function register() { registerHighlight({ projectID: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID, otlpEndpoint: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT, }) }
You'll need to do a conditional import if you're using Next Middleware.
// instrumentation.ts import CONSTANTS from '@/app/constants' export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { /** * Conditional import required for use with Next middleware * * Avoids the following error: * An error occurred while loading instrumentation hook: (0 , _highlight_run_next__WEBPACK_IMPORTED_MODULE_1__.registerHighlight) is not a function */ const { registerHighlight } = await import('@highlight-run/next') registerHighlight({ projectID: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID, otlpEndpoint: CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_OTLP_ENDPOINT, }) } }
Instrument the client
This implementation requires React 17 or greater. If you're behind on React versions, follow our React.js docs
/pages directory, you'll want to add HighlightInit to _app.tsx.// pages/_app.tsx import { AppProps } from 'next/app' import CONSTANTS from '@/app/constants' import { HighlightInit } from '@highlight-run/next/highlight-init' export default function MyApp({ Component, pageProps }: AppProps) { return ( <> <HighlightInit projectId={CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID} tracingOrigins networkRecording={{ enabled: true, recordHeadersAndBody: true, urlBlocklist: [], }} backendUrl={CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL} /> <Component {...pageProps} /> </> ) }
HighlightInit to your layout.tsx file.// src/app/layout.tsx import './globals.css' import CONSTANTS from '@/app/constants' import { HighlightInit } from '@highlight-run/next/highlight-init' export const metadata = { title: 'Highlight Next Demo', description: 'Check out how Highlight works with Next.js', } export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <> <HighlightInit projectId={CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID} tracingOrigins networkRecording={{ enabled: true, recordHeadersAndBody: true, urlBlocklist: [], }} backendUrl={CONSTANTS.NEXT_PUBLIC_HIGHLIGHT_BACKEND_URL} /> <html lang="en"> <body>{children}</body> </html> </> ) }
Configure tracingOrigins and networkRecording
See Fullstack Mapping for details.
You likely want to associate your back-end errors to client sessions.
Test source maps
We recommend shipping your source maps to your production server. Your client-side JavaScript is always public, and code decompilation tools are so powerful that obscuring your source code may not be helpful.
Shipping source maps to production with Next.js is as easy as setting productionBrowserSourceMaps: true in your nextConfig.
Alternatively, you can upload source maps directly to Highlight using our withHighlightConfig function.
Make sure to implement nextConfig.generateBuildId so that our source map uploader can version your source maps correctly. Make sure to omit productionBrowserSourceMaps or set it to false to enable the source map uploader.
// next.config.js const nextBuildId = require('next-build-id') const { withHighlightConfig } = require('@highlight-run/next') /** @type {import('next').NextConfig} */ const nextConfig = { generateBuildId: () => nextBuildId({ dir: __dirname }), experimental: { appDir: true, instrumentationHook: true, }, productionBrowserSourceMaps: false } module.exports = withHighlightConfig(nextConfig)
You must export your HIGHLIGHT_SOURCEMAP_UPLOAD_API_KEY to your build process. If you're building and deploying with Vercel, try our Highlight Vercel Integration to inject HIGHLIGHT_SOURCEMAP_UPLOAD_API_KEY automatically.
Vercel Log Drain
Vercel Log Drain works great. Install our Vercel + Highlight Integration to enable it.