Skip to content

Node profile frames never symbolicated with Turbopack native debug IDs, because the profiler gates resource collection on _sentryDebugIds #24202

Description

@InterstellarStella

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

10.73.0

Framework Version

Next.js 16.3.0 (Turbopack)

Link to Sentry event

No response

Reproduction Example/SDK Setup

next.config.ts:

import { withSentryConfig } from '@sentry/nextjs';

export default withSentryConfig(
  {
    output: 'standalone',
    // Turbopack's native debug ID generation
    turbopack: { debugIds: true },
    serverExternalPackages: ['@sentry/profiling-node'],
  },
  {
    org: process.env.SENTRY_ORG,
    project: process.env.SENTRY_PROJECT,
    authToken: process.env.SENTRY_AUTH_TOKEN,
    release: { name: process.env.SENTRY_RELEASE },
    widenClientFileUpload: true,
    sourcemaps: { deleteSourcemapsAfterUpload: false },
  },
);

instrumentation.ts:

import * as Sentry from '@sentry/nextjs';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  profileSessionSampleRate: 1.0,
  profileLifecycle: 'trace',
  integrations: [nodeProfilingIntegration()],
});

Steps to Reproduce

  1. Build a Next.js 16 app with Turbopack, turbopack: { debugIds: true } and output: 'standalone', using @sentry/nextjs and @sentry/profiling-node 10.73.0 with server-side continuous profiling.
  2. Build for production and deploy the standalone output. Source map upload runs through runAfterProductionCompile and the emitted server chunks carry both a //# debugId= comment and the native registration snippet that writes globalThis._debugIds.
  3. Send traffic so the same process produces both an error event and a profile chunk.
  4. Compare debug_meta on the error event with debug_meta on the profile chunk, and open the flamegraph.

Expected Result

The profile chunk should carry a populated debug_meta.images, the same way the error event does, so the flamegraph resolves to original file paths and function names.

Actual Result

The error event symbolicates correctly. Its debug_meta contains the right image for the server chunk:

{
  "images": [
    {
      "code_file": "app:///_next/server/chunks/[root-of-the-server]__aaaaaaa._.js",
      "debug_id": "00000000-0000-0000-0000-000000000000",
      "type": "sourcemap"
    }
  ]
}

The profile chunk from the same process, same release and same server chunk arrives with:

"debug_meta": {}

Flamegraph frames stay as minified identifiers (oV, aN, eQ) with raw chunk paths such as /srv/app/.next/server/chunks/[root-of-the-server]__aaaaaaa._.js:11:64839. Because debug_meta is empty, the backend skips profile symbolication entirely rather than failing at it.

Additional Context

With turbopack.debugIds enabled, @sentry/nextjs treats debug IDs as natively provided and skips injectDebugIds in handleRunAfterProductionCompile.ts. Nothing writes globalThis._sentryDebugIds in that case, only Turbopack's globalThis._debugIds. Error events are unaffected because getFilenameToDebugIdMap in packages/core/src/utils/debug-ids.ts reads both globals.

The profiler doesn't. @sentry/node-cpu-profiler 2.4.3 decides whether to collect script resources from the legacy global alone:

// lib/index.js
return PrivateCpuProfilerBindings.stopProfiling(name, format, threadId, !!global._sentryDebugIds);

That argument is collect_resources in the binding, and TranslateProfile substitutes an empty array for resources when it is false. createProfileChunkPayload derives the images from exactly that array via applyDebugMetadata(client, cpuProfile.resources), so the chunk leaves the process with nothing to symbolicate.

Resolution itself is fine. Taking the registration snippet out of a real server chunk and passing that chunk's own path through getDebugImagesForResources returns the correct image, so the gate looks like the only broken part.

Setting globalThis._sentryDebugIds = globalThis._sentryDebugIds || {} before Sentry.init works around it, since the empty object passes the check and the merged map still resolves the native IDs.

Worth noting separately that frames from ESM packages under node_modules arrive as file:///... paths, which the backend rejects per getsentry/sentry#109520. Turbopack's own server chunks use plain absolute paths and aren't affected by that one.

Priority

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions