# Next.js

> Learn how to integrate OpenTelemetry with Next.js applications and send telemetry data to Last9

Source: https://last9.io/docs/integrations/frameworks/javascript/nextjs/

This guide shows you how to instrument your Next.js application with OpenTelemetry and send traces, metrics, and logs to Last9. This covers both App Router (13+) and Pages Router approaches.

## Prerequisites

- Node.js 16 or later
- Next.js 12+ application
- Last9 account with OTLP endpoint configured

## Installation

1. **Install OpenTelemetry packages**

   ```bash
   npm install --save @opentelemetry/api \
     @opentelemetry/sdk-node \
     @opentelemetry/auto-instrumentations-node \
     @opentelemetry/exporter-trace-otlp-http \
     @opentelemetry/exporter-metrics-otlp-http \
     @opentelemetry/exporter-logs-otlp-http \
     @opentelemetry/resources \
     @opentelemetry/semantic-conventions
   ```

2. **Install Next.js specific packages (optional for enhanced features)**

   ```bash
   npm install --save @vercel/otel \
     @opentelemetry/instrumentation-http \
     @opentelemetry/instrumentation-fetch \
     @opentelemetry/instrumentation-fs
   ```

## Configuration

Set up your environment variables:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
export OTEL_SERVICE_NAME="nextjs-app"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"
export NODE_ENV="production"
```

## Server-Side Instrumentation

### Enable Next.js Instrumentation Hook

**next.config.js**

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    instrumentationHook: true,
  },
  // Optional: Enable OpenTelemetry support
  experimental: {
    serverComponentsExternalPackages: ["@opentelemetry/api"],
  },
};

module.exports = nextConfig;
```

**next.config.ts**

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    instrumentationHook: true,
    serverComponentsExternalPackages: ["@opentelemetry/api"],
  },
};

export default nextConfig;
```

### Core Instrumentation Setup

**instrumentation.ts**

```typescript
export async function register() {
  // Only register instrumentation in Node.js environment (server-side)
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./instrumentation.node");
  }
}
```

**instrumentation.node.ts**

```typescript
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
import {
  BatchSpanProcessor,
  SpanProcessor,
  Span,
} from "@opentelemetry/sdk-trace-base";
import * as api from "@opentelemetry/api";
import {
  W3CTraceContextPropagator,
  W3CBaggagePropagator,
  CompositePropagator,
} from "@opentelemetry/core";

// For troubleshooting, uncomment the following lines to enable OpenTelemetry debug logging:
// import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

// BaggageSpanProcessor - propagates W3C Baggage entries as span attributes for RUM correlation
class BaggageSpanProcessor implements SpanProcessor {
  onStart(span: Span, parentContext: api.Context): void {
    const baggage = api.propagation.getBaggage(
      parentContext || api.context.active(),
    );
    if (!baggage) return;
    for (const [key, entry] of baggage.getAllEntries()) {
      span.setAttribute(key, entry.value);
    }
  }
  onEnd(): void {}
  forceFlush(): Promise<void> {
    return Promise.resolve();
  }
  shutdown(): Promise<void> {
    return Promise.resolve();
  }
}

// Create resource with service information
const resource = resourceFromAttributes({
  [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "nextjs-app",
  "service.version": process.env.npm_package_version || "1.0.0",
  "deployment.environment": process.env.NODE_ENV || "development",
});

// Initialize the SDK
// SDK auto-reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS from env
const sdk = new NodeSDK({
  resource,
  textMapPropagator: new CompositePropagator({
    propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
  }),
  spanProcessors: [
    new BaggageSpanProcessor(),
    new BatchSpanProcessor(new OTLPTraceExporter()),
  ],
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
    exportIntervalMillis: 30000,
  }),
  logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
  instrumentations: [
    getNodeAutoInstrumentations({
      // Disable file system instrumentation as it's too noisy for Next.js
      "@opentelemetry/instrumentation-fs": {
        enabled: false,
      },
      // Disable DNS instrumentation to reduce noise
      "@opentelemetry/instrumentation-dns": {
        enabled: false,
      },
      // Configure HTTP instrumentation
      "@opentelemetry/instrumentation-http": {
        enabled: true,
        ignoreIncomingRequestHook: (req) => {
          // Ignore health checks and static assets
          const url = req.url || "";
          return (
            url.includes("/_next/") ||
            url.includes("/favicon.ico") ||
            url.includes("/health") ||
            url.includes("/api/health")
          );
        },
      },
      // Configure fetch instrumentation for server-side API calls
      "@opentelemetry/instrumentation-fetch": {
        enabled: true,
        ignoreUrls: [/\/_next\//, /\/favicon\.ico/, /\/health/],
      },
    }),
  ],
});

// sdk.start() is synchronous in sdk-node 0.201.x+
try {
  sdk.start();
  console.log("[OpenTelemetry] Next.js instrumentation initialized");
} catch (error) {
  console.error("[OpenTelemetry] Failed to initialize tracing", error);
}

process.on("SIGTERM", () => sdk.shutdown().finally(() => process.exit(0)));
process.on("SIGINT", () => sdk.shutdown().finally(() => process.exit(0)));
```

### Enhanced Instrumentation with Custom Telemetry

**lib/telemetry.ts**

```typescript
import { trace, metrics, context } from "@opentelemetry/api";
import type { Span, Tracer, Meter } from "@opentelemetry/api";

// Get tracer and meter instances
export const tracer: Tracer = trace.getTracer("nextjs-app", "1.0.0");
export const meter: Meter = metrics.getMeter("nextjs-app", "1.0.0");

// Create custom metrics
export const nextjsMetrics = {
  pageViews: meter.createCounter("nextjs_page_views_total", {
    description: "Total number of page views",
  }),

  apiCalls: meter.createCounter("nextjs_api_calls_total", {
    description: "Total number of API calls",
  }),

  renderDuration: meter.createHistogram("nextjs_render_duration_ms", {
    description: "Duration of page renders in milliseconds",
    unit: "ms",
  }),

  hydrationDuration: meter.createHistogram("nextjs_hydration_duration_ms", {
    description: "Duration of client-side hydration in milliseconds",
    unit: "ms",
  }),

  errorCount: meter.createCounter("nextjs_errors_total", {
    description: "Total number of errors",
  }),

  cacheHits: meter.createCounter("nextjs_cache_hits_total", {
    description: "Total number of cache hits",
  }),

  cacheMisses: meter.createCounter("nextjs_cache_misses_total", {
    description: "Total number of cache misses",
  }),
};

// Utility functions for common tracing patterns
export const withTracing = async <T>(
  name: string,
  fn: (span: Span) => Promise<T>,
  attributes: Record<string, string | number | boolean> = {},
): Promise<T> => {
  return tracer.startActiveSpan(name, async (span) => {
    try {
      // Set attributes
      Object.entries(attributes).forEach(([key, value]) => {
        span.setAttributes({ [key]: value });
      });

      const result = await fn(span);

      span.setStatus({ code: 1 }); // OK
      return result;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: 2, message: (error as Error).message }); // ERROR
      throw error;
    } finally {
      span.end();
    }
  });
};

export const getCurrentSpan = (): Span | undefined => {
  return trace.getActiveSpan();
};

export const addSpanAttributes = (
  attributes: Record<string, string | number | boolean>,
): void => {
  const span = getCurrentSpan();
  if (span) {
    span.setAttributes(attributes);
  }
};
```

**lib/middleware-telemetry.ts**

```typescript
import { NextRequest, NextResponse } from "next/server";
import { tracer, nextjsMetrics, addSpanAttributes } from "./telemetry";

export function withTelemetryMiddleware(
  handler: (req: NextRequest) => Promise<NextResponse> | NextResponse,
) {
  return async (req: NextRequest): Promise<NextResponse> => {
    const startTime = Date.now();

    return tracer.startActiveSpan("nextjs.middleware", async (span) => {
      try {
        // Set basic span attributes
        span.setAttributes({
          "http.method": req.method,
          "http.url": req.url,
          "http.route": req.nextUrl.pathname,
          "user_agent.original": req.headers.get("user-agent") || "",
        });

        // Execute the handler
        const response = await handler(req);

        // Record metrics
        const duration = Date.now() - startTime;
        const statusCode = response.status;

        nextjsMetrics.apiCalls.add(1, {
          method: req.method,
          route: req.nextUrl.pathname,
          status_code: statusCode.toString(),
        });

        // Set response attributes
        span.setAttributes({
          "http.status_code": statusCode,
          "http.response.size": response.headers.get("content-length") || 0,
        });

        return response;
      } catch (error) {
        span.recordException(error as Error);
        span.setStatus({ code: 2, message: (error as Error).message });

        nextjsMetrics.errorCount.add(1, {
          error_type: "middleware_error",
          route: req.nextUrl.pathname,
        });

        throw error;
      }
    });
  };
}
```

## App Router (Next.js 13+) Integration

### Server Components

**app/users/page.tsx**

```typescript
import { tracer, nextjsMetrics, withTracing } from '@/lib/telemetry'
import { Suspense } from 'react'

async function fetchUsers() {
  return withTracing('users.fetch_all', async (span) => {
    const startTime = Date.now()

    try {
      // Simulate API call
      const response = await fetch('https://jsonplaceholder.typicode.com/users')

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const users = await response.json()

      span.setAttributes({
        'users.count': users.length,
        'http.status_code': response.status,
        'cache.hit': response.headers.get('x-cache') === 'HIT',
      })

      // Record cache metrics
      if (response.headers.get('x-cache') === 'HIT') {
        nextjsMetrics.cacheHits.add(1, { type: 'users' })
      } else {
        nextjsMetrics.cacheMisses.add(1, { type: 'users' })
      }

      return users
    } catch (error) {
      nextjsMetrics.errorCount.add(1, {
        error_type: 'fetch_error',
        operation: 'fetch_users',
      })
      throw error
    } finally {
      const duration = Date.now() - startTime
      nextjsMetrics.renderDuration.record(duration, {
        component: 'users_page',
        operation: 'fetch_users',
      })
    }
  })
}

async function UsersList() {
  const users = await fetchUsers()

  return (
    <div className="grid gap-4">
      {users.map((user: any) => (
        <div key={user.id} className="p-4 border rounded">
          <h3 className="font-bold">{user.name}</h3>
          <p className="text-gray-600">{user.email}</p>
          <p className="text-sm">{user.company.name}</p>
        </div>
      ))}
    </div>
  )
}

export default async function UsersPage() {
  // Record page view
  nextjsMetrics.pageViews.add(1, {
    page: '/users',
    type: 'server_component',
  })

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Users</h1>
      <Suspense fallback={<div>Loading users...</div>}>
        <UsersList />
      </Suspense>
    </div>
  )
}

// Generate metadata with tracing
export async function generateMetadata() {
  return withTracing('users.generate_metadata', async (span) => {
    span.setAttributes({
      'metadata.page': '/users',
      'metadata.type': 'dynamic',
    })

    return {
      title: 'Users - Next.js App',
      description: 'List of all users',
    }
  })
}
```

**app/users/[id]/page.tsx**

```typescript
import { tracer, nextjsMetrics, withTracing } from '@/lib/telemetry'
import { notFound } from 'next/navigation'

interface User {
  id: number
  name: string
  email: string
  phone: string
  website: string
  company: {
    name: string
    catchPhrase: string
  }
}

async function fetchUser(id: string): Promise<User | null> {
  return withTracing('user.fetch_by_id', async (span) => {
    span.setAttributes({
      'user.id': id,
      'operation': 'fetch_user',
    })

    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)

      if (response.status === 404) {
        span.setAttributes({ 'user.found': false })
        return null
      }

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const user = await response.json()

      span.setAttributes({
        'user.found': true,
        'user.name': user.name,
        'http.status_code': response.status,
      })

      return user
    } catch (error) {
      nextjsMetrics.errorCount.add(1, {
        error_type: 'fetch_error',
        operation: 'fetch_user',
        user_id: id,
      })
      throw error
    }
  })
}

export default async function UserPage({ params }: { params: { id: string } }) {
  // Record page view
  nextjsMetrics.pageViews.add(1, {
    page: `/users/${params.id}`,
    type: 'server_component',
  })

  const user = await fetchUser(params.id)

  if (!user) {
    notFound()
  }

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="max-w-2xl">
        <h1 className="text-3xl font-bold mb-8">{user.name}</h1>

        <div className="space-y-4">
          <div>
            <h3 className="font-semibold">Email</h3>
            <p>{user.email}</p>
          </div>

          <div>
            <h3 className="font-semibold">Phone</h3>
            <p>{user.phone}</p>
          </div>

          <div>
            <h3 className="font-semibold">Website</h3>
            <p>{user.website}</p>
          </div>

          <div>
            <h3 className="font-semibold">Company</h3>
            <p>{user.company.name}</p>
            <p className="text-sm text-gray-600">{user.company.catchPhrase}</p>
          </div>
        </div>
      </div>
    </div>
  )
}

// Static generation with tracing
export async function generateStaticParams() {
  return withTracing('users.generate_static_params', async (span) => {
    // Generate first 10 user IDs for static generation
    const params = Array.from({ length: 10 }, (_, i) => ({ id: (i + 1).toString() }))

    span.setAttributes({
      'static_params.count': params.length,
      'static_params.type': 'user_ids',
    })

    return params
  })
}
```

### API Routes

**app/api/users/route.ts**

```typescript
import { NextRequest, NextResponse } from "next/server";
import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";

export async function GET(request: NextRequest) {
  return withTracing("api.users.get", async (span) => {
    const startTime = Date.now();

    try {
      // Parse query parameters
      const { searchParams } = new URL(request.url);
      const limit = searchParams.get("limit") || "10";
      const page = searchParams.get("page") || "1";

      span.setAttributes({
        "api.operation": "list_users",
        "api.limit": parseInt(limit),
        "api.page": parseInt(page),
      });

      // Simulate database query
      await new Promise((resolve) => setTimeout(resolve, 100));

      const users = Array.from({ length: parseInt(limit) }, (_, i) => ({
        id: i + 1 + (parseInt(page) - 1) * parseInt(limit),
        name: `User ${i + 1}`,
        email: `user${i + 1}@example.com`,
      }));

      span.setAttributes({
        "users.count": users.length,
        "response.size": JSON.stringify(users).length,
      });

      // Record metrics
      const duration = Date.now() - startTime;
      nextjsMetrics.apiCalls.add(1, {
        method: "GET",
        route: "/api/users",
        status_code: "200",
      });

      nextjsMetrics.renderDuration.record(duration, {
        component: "api_users",
        operation: "list",
      });

      return NextResponse.json({
        users,
        pagination: {
          page: parseInt(page),
          limit: parseInt(limit),
          total: users.length,
        },
      });
    } catch (error) {
      span.recordException(error as Error);

      nextjsMetrics.errorCount.add(1, {
        error_type: "api_error",
        operation: "list_users",
      });

      return NextResponse.json(
        { error: "Failed to fetch users" },
        { status: 500 },
      );
    }
  });
}

export async function POST(request: NextRequest) {
  return withTracing("api.users.create", async (span) => {
    const startTime = Date.now();

    try {
      const body = await request.json();

      span.setAttributes({
        "api.operation": "create_user",
        "user.email": body.email,
        "request.body_size": JSON.stringify(body).length,
      });

      // Validate required fields
      if (!body.name || !body.email) {
        span.setAttributes({
          "validation.failed": true,
          "validation.missing_fields": [
            !body.name && "name",
            !body.email && "email",
          ]
            .filter(Boolean)
            .join(","),
        });

        return NextResponse.json(
          { error: "Name and email are required" },
          { status: 400 },
        );
      }

      // Simulate user creation
      await new Promise((resolve) => setTimeout(resolve, 200));

      const newUser = {
        id: Date.now(),
        name: body.name,
        email: body.email,
        createdAt: new Date().toISOString(),
      };

      span.setAttributes({
        "user.created": true,
        "user.id": newUser.id,
      });

      // Record metrics
      const duration = Date.now() - startTime;
      nextjsMetrics.apiCalls.add(1, {
        method: "POST",
        route: "/api/users",
        status_code: "201",
      });

      nextjsMetrics.renderDuration.record(duration, {
        component: "api_users",
        operation: "create",
      });

      return NextResponse.json(newUser, { status: 201 });
    } catch (error) {
      span.recordException(error as Error);

      nextjsMetrics.errorCount.add(1, {
        error_type: "api_error",
        operation: "create_user",
      });

      return NextResponse.json(
        { error: "Failed to create user" },
        { status: 500 },
      );
    }
  });
}
```

**app/api/users/[id]/route.ts**

```typescript
import { NextRequest, NextResponse } from "next/server";
import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } },
) {
  return withTracing("api.users.get_by_id", async (span) => {
    const startTime = Date.now();

    try {
      const userId = params.id;

      span.setAttributes({
        "api.operation": "get_user",
        "user.id": userId,
      });

      // Validate user ID
      if (!userId || isNaN(parseInt(userId))) {
        span.setAttributes({
          "validation.failed": true,
          "validation.error": "invalid_user_id",
        });

        return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
      }

      // Simulate database query
      await new Promise((resolve) => setTimeout(resolve, 50));

      // Simulate user not found for IDs > 100
      if (parseInt(userId) > 100) {
        span.setAttributes({
          "user.found": false,
        });

        return NextResponse.json({ error: "User not found" }, { status: 404 });
      }

      const user = {
        id: parseInt(userId),
        name: `User ${userId}`,
        email: `user${userId}@example.com`,
        createdAt: new Date().toISOString(),
      };

      span.setAttributes({
        "user.found": true,
        "user.name": user.name,
      });

      // Record metrics
      const duration = Date.now() - startTime;
      nextjsMetrics.apiCalls.add(1, {
        method: "GET",
        route: "/api/users/[id]",
        status_code: "200",
      });

      nextjsMetrics.renderDuration.record(duration, {
        component: "api_user_detail",
        operation: "get",
      });

      return NextResponse.json(user);
    } catch (error) {
      span.recordException(error as Error);

      nextjsMetrics.errorCount.add(1, {
        error_type: "api_error",
        operation: "get_user",
        user_id: params.id,
      });

      return NextResponse.json(
        { error: "Failed to fetch user" },
        { status: 500 },
      );
    }
  });
}
```

## Client-Side Instrumentation

### Web Vitals and User Experience

**lib/web-vitals.ts**

```typescript
import { getCLS, getFID, getFCP, getLCP, getTTFB, Metric } from "web-vitals";

// Custom function to send metrics to your analytics endpoint
function sendToAnalytics(metric: Metric, labels: Record<string, string> = {}) {
  // Send to your API endpoint or directly to Last9
  fetch("/api/analytics/web-vitals", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      id: metric.id,
      delta: metric.delta,
      rating: metric.rating,
      ...labels,
    }),
  }).catch(console.error);
}

export function reportWebVitals() {
  const labels = {
    page: window.location.pathname,
    referrer: document.referrer || "direct",
    user_agent: navigator.userAgent,
  };

  getCLS((metric) => sendToAnalytics(metric, labels));
  getFID((metric) => sendToAnalytics(metric, labels));
  getFCP((metric) => sendToAnalytics(metric, labels));
  getLCP((metric) => sendToAnalytics(metric, labels));
  getTTFB((metric) => sendToAnalytics(metric, labels));
}
```

**app/api/analytics/web-vitals/route.ts**

```typescript
import { NextRequest, NextResponse } from "next/server";
import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";

export async function POST(request: NextRequest) {
  return withTracing("api.analytics.web_vitals", async (span) => {
    try {
      const metrics = await request.json();

      span.setAttributes({
        "web_vitals.metric": metrics.name,
        "web_vitals.value": metrics.value,
        "web_vitals.rating": metrics.rating,
        "web_vitals.page": metrics.page,
      });

      // Record web vitals as custom metrics
      const metricName = `nextjs_web_vitals_${metrics.name.toLowerCase()}`;

      // Create or get the metric counter/histogram
      const webVitalMetric = nextjsMetrics.renderDuration; // Reuse existing histogram

      webVitalMetric.record(metrics.value, {
        metric_name: metrics.name,
        page: metrics.page,
        rating: metrics.rating,
      });

      return NextResponse.json({ success: true });
    } catch (error) {
      span.recordException(error as Error);

      nextjsMetrics.errorCount.add(1, {
        error_type: "web_vitals_error",
        operation: "record_metrics",
      });

      return NextResponse.json(
        { error: "Failed to record web vitals" },
        { status: 500 },
      );
    }
  });
}
```

**app/layout.tsx**

```typescript
'use client'

import { useEffect } from 'react'
import { reportWebVitals } from '@/lib/web-vitals'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  useEffect(() => {
    // Initialize web vitals reporting
    reportWebVitals()
  }, [])

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
```

## Pages Router Integration

**pages/api/legacy-users.ts**

```typescript
import type { NextApiRequest, NextApiResponse } from "next";
import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  return withTracing("pages_api.legacy_users", async (span) => {
    const startTime = Date.now();

    try {
      span.setAttributes({
        "api.method": req.method,
        "api.route": "/api/legacy-users",
        "api.type": "pages_router",
      });

      if (req.method === "GET") {
        // Simulate database query
        await new Promise((resolve) => setTimeout(resolve, 100));

        const users = Array.from({ length: 5 }, (_, i) => ({
          id: i + 1,
          name: `Legacy User ${i + 1}`,
          email: `legacy${i + 1}@example.com`,
        }));

        span.setAttributes({
          "users.count": users.length,
          operation: "list_users",
        });

        // Record metrics
        const duration = Date.now() - startTime;
        nextjsMetrics.apiCalls.add(1, {
          method: "GET",
          route: "/api/legacy-users",
          status_code: "200",
          router_type: "pages",
        });

        nextjsMetrics.renderDuration.record(duration, {
          component: "pages_api_users",
          operation: "list",
        });

        res.status(200).json({ users });
      } else {
        span.setAttributes({
          "error.type": "method_not_allowed",
        });

        res.setHeader("Allow", ["GET"]);
        res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    } catch (error) {
      span.recordException(error as Error);

      nextjsMetrics.errorCount.add(1, {
        error_type: "pages_api_error",
        operation: "legacy_users",
      });

      res.status(500).json({ error: "Internal Server Error" });
    }
  });
}
```

**pages/legacy-users.tsx**

```typescript
import { GetServerSideProps } from 'next'
import { tracer, nextjsMetrics, withTracing } from '@/lib/telemetry'

interface User {
  id: number
  name: string
  email: string
}

interface Props {
  users: User[]
}

export default function LegacyUsersPage({ users }: Props) {
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Legacy Users</h1>
      <div className="grid gap-4">
        {users.map((user) => (
          <div key={user.id} className="p-4 border rounded">
            <h3 className="font-bold">{user.name}</h3>
            <p className="text-gray-600">{user.email}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  return withTracing('pages.legacy_users.get_server_side_props', async (span) => {
    const startTime = Date.now()

    try {
      span.setAttributes({
        'pages.route': '/legacy-users',
        'pages.type': 'server_side_props',
      })

      // Simulate API call
      const users = Array.from({ length: 10 }, (_, i) => ({
        id: i + 1,
        name: `Legacy User ${i + 1}`,
        email: `legacy${i + 1}@example.com`,
      }))

      span.setAttributes({
        'users.count': users.length,
        'data_fetched': true,
      })

      // Record metrics
      const duration = Date.now() - startTime
      nextjsMetrics.pageViews.add(1, {
        page: '/legacy-users',
        type: 'server_side_props',
      })

      nextjsMetrics.renderDuration.record(duration, {
        component: 'legacy_users_page',
        operation: 'get_server_side_props',
      })

      return {
        props: {
          users,
        },
      }

    } catch (error) {
      span.recordException(error as Error)

      nextjsMetrics.errorCount.add(1, {
        error_type: 'ssr_error',
        page: '/legacy-users',
      })

      // Return error props or redirect
      return {
        props: {
          users: [],
        },
      }
    }
  })
}
```

## Production Deployment

### Environment Configuration

**.env.production**

```bash
# OpenTelemetry Configuration
OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header
OTEL_SERVICE_NAME=nextjs-app
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0

# Next.js Configuration
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1

# Optional: Custom configurations
DATABASE_URL=your-database-url
REDIS_URL=your-redis-url
```

**Dockerfile**

```dockerfile
FROM node:18-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME 0.0.0.0

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:3000/api/health || exit 1

CMD ["node", "server.js"]
```

**docker-compose.yml**

```yaml
version: "3.8"

services:
  nextjs-app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
      - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header
      - OTEL_SERVICE_NAME=nextjs-app
      - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0
      - DATABASE_URL=postgresql://user:pass@postgres:5432/nextjs_db
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: nextjs_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
```

Use the same service and environment values across Next.js traces, ECS logs, and dashboards. If your ECS task definition emits logs with `service.name=checkout-service` and `deployment.environment=production`, set `OTEL_SERVICE_NAME=checkout-service` and include `deployment.environment=production` in `OTEL_RESOURCE_ATTRIBUTES`. Mismatched values split one application across multiple services or environments in Last9.

## Testing the Integration

1. **Start your application**

   ```bash
   npm run dev
   # or for production
   npm run build && npm start
   ```

2. **Test different routes**

   ```bash
   # App Router pages
   curl http://localhost:3000/users
   curl http://localhost:3000/users/1

   # API routes
   curl http://localhost:3000/api/users
   curl http://localhost:3000/api/users/1

   # Pages Router (if implemented)
   curl http://localhost:3000/legacy-users
   curl http://localhost:3000/api/legacy-users
   ```

3. **View telemetry in Last9**

   Check your Last9 dashboard for:

   - Server-side rendering traces with Next.js specific attributes
   - API route spans with proper context propagation
   - Client-side web vitals metrics
   - Database and external API call traces
   - Custom business logic spans and metrics

---

## Troubleshooting

- **No traces appearing in Last9.** Confirm `instrumentation.ts` is at the project root (or `src/`) and exports a `register()` function. On Next.js versions below 15, enable `experimental.instrumentationHook` in `next.config.js`. Verify `OTEL_EXPORTER_OTLP_ENDPOINT` and the Last9 authorization header are set in the environment the app actually runs in.
- **Instrumentation hook not firing.** The hook runs only in the Node.js runtime. If a route sets `export const runtime = "edge"`, the OpenTelemetry Node SDK will not initialize there. Keep instrumented routes on the Node.js runtime.
- **Client-side Web Vitals not reported.** Web Vitals are collected in the browser, so confirm the client instrumentation loads on the page and that an ad blocker or Content Security Policy is not blocking requests to the Last9 endpoint.
- **Duplicate or missing spans.** Running more than one OpenTelemetry SDK (for example, a platform-provided tracer alongside this setup) double-instruments requests. Initialize a single tracer provider and ensure the SDK is not registered more than once.
- **One application split across multiple services or environments.** Set the same `OTEL_SERVICE_NAME` and `deployment.environment` values across server traces, ECS logs, and dashboards. Mismatched values split one application into separate services or environments in Last9.
- **Spans not exported on serverless or Vercel deployments.** Short-lived functions can exit before telemetry is flushed. Flush the span processor before the function returns, or use a setup that forces a flush on shutdown.

Please get in touch with us on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io) if you have any questions.
