getsentry / sentry-javascript

Official Sentry SDKs for JavaScript
https://sentry.io
MIT License
7.94k stars 1.56k forks source link

Multiple trace IDs generated in sentry-trace header when using Next.js #13870

Open shlyamster opened 1 week ago

shlyamster commented 1 week ago

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/nextjs

SDK Version

8.33.1

Framework Version

Next 14.2.14

Link to Sentry event

n/a

Reproduction Example/SDK Setup

Dependencies

{
  "dependencies": {
    "@next/third-parties": "^14.2.14",
    "@nextui-org/react": "2.4.2",
    "@nextui-org/use-infinite-scroll": "^2.1.5",
    "@nimpl/middleware-chain": "^0.4.0",
    "@sentry/nextjs": "^8.33.1",
    "caniuse-lite": "^1.0.30001667",
    "clsx": "^2.1.1",
    "dayjs": "^1.11.13",
    "framer-motion": "^11.11.0",
    "intl-locale-textinfo-polyfill": "^2.1.1",
    "negotiator": "^0.6.3",
    "next": "^14.2.14",
    "next-mdx-remote": "^5.0.0",
    "next-seo": "^6.6.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.53.0",
    "sharp": "^0.33.5",
    "swr": "^2.2.5",
    "uuid": "^10.0.0"
  },
  "devDependencies": {
    "@next/bundle-analyzer": "^14.2.14",
    "@svgr/webpack": "^8.1.0",
    "@trivago/prettier-plugin-sort-imports": "^4.3.0",
    "@types/node": "^22.7.4",
    "@types/react": "^18.3.11",
    "@types/react-dom": "^18.3.0",
    "@types/uuid": "^10.0.0",
    "autoprefixer": "^10.4.20",
    "cssnano": "^7.0.6",
    "cssnano-preset-advanced": "^7.0.6",
    "eslint": "^8.57.1",
    "eslint-config-next": "^14.2.14",
    "postcss": "^8.4.47",
    "prettier": "^3.3.3",
    "prettier-plugin-tailwindcss": "^0.6.8",
    "tailwindcss": "^3.4.13",
    "typescript": "^5.6.2"
  }
}

sentry.client.config.ts

import * as Sentry from '@sentry/nextjs';

import { API_BASE_URL, APP_BASE_URL, ENVIRONMENT, RELEASE, SENTRY_DSN, SENTRY_TRACES_SAMPLE_RATE } from '@/config';

Sentry.init({
  dsn: SENTRY_DSN,
  release: RELEASE,
  environment: ENVIRONMENT,
  tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,
  tracePropagationTargets: [APP_BASE_URL, API_BASE_URL],
  integrations: [Sentry.browserTracingIntegration()],
});

instrumentation.ts

import * as Sentry from '@sentry/nextjs';

import { API_BASE_URL, ENVIRONMENT, RELEASE, SENTRY_DSN, SENTRY_TRACES_SAMPLE_RATE } from '@/config';

export async function register() {
  Sentry.init({
    dsn: SENTRY_DSN,
    release: RELEASE,
    environment: ENVIRONMENT,
    tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,
    tracePropagationTargets: [API_BASE_URL],
  });
}

export const onRequestError = Sentry.captureRequestError;

Steps to Reproduce

  1. Enable tracePropagationTargets and configure it to point to your backend server.
  2. Make HTTP requests using fetch inside middleware or during SSG generation in a Next.js project.
  3. When performing an HTTP request, Sentry will add the Baggage and Sentry-Trace headers to the outgoing requests.
  4. These headers will be received by the backend (using sentry-go).
  5. The Sentry-Trace header will contain two trace IDs, for example: 0b7da23d63cc4a7d9436abf37ace0e0c-9510fafae5f1c2c5-1,faca5d26f53d660657d9e4b073fb8da2-74dbb120f6aca02a-1
  6. Due to the presence of two trace IDs, the backend cannot correctly parse the Parent Span ID, leading to incorrect transaction linking. Image

Expected Result

The Sentry-Trace header should contain only a single trace ID.

Actual Result

The Sentry-Trace header contains multiple trace IDs.

lforst commented 1 week ago

Hi, thanks for moving this issue here! Before I go and try reproduce this. How are you calling your Next.js app?

shlyamster commented 1 week ago

@lforst My application is running in docker node:22-alpine, started using the node server.js command from the standalone folder. Before that, the Next.js files are built using the next build command. Below is my next.config.js file.

const packageInfo = require('./package.json');
const { PHASE_PRODUCTION_SERVER } = require('next/constants');
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' });
const { withSentryConfig } = require('@sentry/nextjs');

const parseEnv = variable =>
  variable === undefined ||
  variable === null ||
  variable === '' ||
  variable === 'undefined' ||
  variable === 'null' ||
  variable === '<nil>'
    ? undefined
    : variable;

const buildId = parseEnv(process.env.NEXT_PUBLIC_BUILD_ID);
const environment = parseEnv(process.env.NODE_ENV) || 'development';
const release = `${packageInfo.name}@${packageInfo.version}+${buildId || environment}`;

const appBaseUrl = new URL(parseEnv(process.env.NEXT_PUBLIC_APP_BASE_URL));
const apiBaseUrl = new URL(parseEnv(process.env.NEXT_PUBLIC_API_BASE_URL));

module.exports = phase => {
  const isServer = phase === PHASE_PRODUCTION_SERVER;

  /** @type {import('next').NextConfig} */
  const nextConfig = {
    generateBuildId: () => buildId || environment,
    output: buildId ? 'standalone' : undefined,
    compress: !buildId,
    cacheHandler: isServer && buildId ? require.resolve('./cache-handler.mjs') : undefined,
    cacheMaxMemorySize: isServer && buildId ? 0 : undefined,
    logging: {
      fetches: {
        fullUrl: !buildId,
      },
    },
    poweredByHeader: false,
    experimental: {
      instrumentationHook: true,
      serverActions: { allowedOrigins: appBaseUrl ? [appBaseUrl.host] : undefined },
    },
    transpilePackages: ['next-mdx-remote'],
    images: {
      minimumCacheTTL: 604800,
      formats: ['image/avif', 'image/webp'],
      deviceSizes: [375, 640, 768, 828, 1024, 1080, 1280, 1440, 1570, 1920, 2048, 2560],
    },
  };

  /** @type {Parameters<import('@sentry/nextjs').withSentryConfig>[1]} */
  const sentryBuildOptions = {
    org: parseEnv(process.env.NEXT_PUBLIC_SENTRY_ORG),
    project: packageInfo.name,
    authToken: parseEnv(process.env.SENTRY_AUTH_TOKEN),
    telemetry: false,
    silent: !!buildId,
    hideSourceMaps: !!buildId,
    disableLogger: !!buildId,
    tunnelRoute: '/api/monitoring-tunnel',
  };

  return withBundleAnalyzer(withSentryConfig(nextConfig, sentryBuildOptions));
};
lforst commented 1 week ago

Ah sorry, I meant how the middleware is invoked as in how the HTTP request to your Next.js app is made 😄

shlyamster commented 1 week ago

@lforst I have middleware enabled for the following routers.

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)'],
};

I have complex business logic to call middleware depending on URL and user authorization. But I always have at least 1 middleware function that is always called. To implement calling different middlewares we use @nimpl/middleware-chain library. My middleware is similar to the example from the library. Unfortunately I can't provide the exact source code of my middleware due to privacy.

lforst commented 1 week ago

What I meant is, what causes your server to be hit to generate these traces? 😂 I assume the answer is "People open our website in their browser".

shlyamster commented 1 week ago

@lforst To reproduce the error it is enough to have any static page created in app directory and at least one middleware via @nimpl/middleware-chain library. After that it is enough to visit this page, when GET request to this page will be called and processed by middleware, which I understand adds an additional Trace ID. Unfortunately I can hardly explain the problem more precisely because I am more a backend developer than a frontend one. If you can tell me how I can do profiling or logging, I will be happy to attach the results from my application.

shlyamster commented 1 week ago

@lforst Additionally, I can add that inside the middleware the fetch call is executed, which receives the Sentry-Trace header with two Trace IDs.

lforst commented 1 week ago

@shlyamster Thanks! I have a hunch, but I don't know for sure whether I am right. Maybe this fixes it: https://github.com/getsentry/sentry-javascript/pull/13907

(Side note: None of this is happening in the frontend so you should be in your happy place as a backend dev 😄)