From e620545c939f0b98b7cc4a6b6317473ff63d0308 Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 19:16:31 +0000 Subject: [PATCH] fix(core): Normalise Sentry stack frame paths to stable app:/// roots (#31421) Co-authored-by: n8n-cat-bot[bot] Co-authored-by: Claude Opus 4.7 --- .../errors/__tests__/error-reporter.test.ts | 69 ++++++++++++++++++- packages/core/src/errors/error-reporter.ts | 25 ++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/packages/core/src/errors/__tests__/error-reporter.test.ts b/packages/core/src/errors/__tests__/error-reporter.test.ts index 8a233462c52..d2369f59a49 100644 --- a/packages/core/src/errors/__tests__/error-reporter.test.ts +++ b/packages/core/src/errors/__tests__/error-reporter.test.ts @@ -6,7 +6,7 @@ import { ApplicationError, BaseError } from 'n8n-workflow'; import type { Mock } from 'vitest'; import { mock } from 'vitest-mock-extended'; -import { ErrorReporter } from '../error-reporter'; +import { ErrorReporter, normalizeFrameFilename } from '../error-reporter'; vi.mock('@sentry/node', () => ({ init: vi.fn(), @@ -25,6 +25,73 @@ vi.mock('@sentry/node-native', () => ({ vi.spyOn(process, 'on'); +describe('normalizeFrameFilename', () => { + it('rewrites pnpm-nested n8n-core frames to a stable app:/// root', () => { + const input = + '/usr/local/lib/node_modules/n8n/node_modules/.pnpm/n8n-core@file+packages+core_abc123/node_modules/n8n-core/src/execution-engine/workflow-execute.ts'; + + expect(normalizeFrameFilename(input)).toBe( + 'app:///n8n-core/src/execution-engine/workflow-execute.ts', + ); + }); + + it('rewrites pnpm-nested n8n-nodes-base frames to a stable app:/// root', () => { + const input = + '/usr/local/lib/node_modules/n8n/node_modules/.pnpm/n8n-nodes-base@1.2.3_xyz789/node_modules/n8n-nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts'; + + expect(normalizeFrameFilename(input)).toBe( + 'app:///n8n-nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts', + ); + }); + + it('rewrites pnpm-nested @n8n scoped frames to a stable app:/// root', () => { + const input = + '/usr/local/lib/node_modules/n8n/node_modules/.pnpm/@n8n+n8n-nodes-langchain@1.0.0_peer+hash/node_modules/@n8n/n8n-nodes-langchain/nodes/agents/Agent.node.ts'; + + expect(normalizeFrameFilename(input)).toBe( + 'app:///@n8n/n8n-nodes-langchain/nodes/agents/Agent.node.ts', + ); + }); + + it('rewrites cli install-prefix frames (src) to a stable app:/// root', () => { + const input = '/usr/local/lib/node_modules/n8n/src/commands/start.ts'; + + expect(normalizeFrameFilename(input)).toBe('app:///src/commands/start.ts'); + }); + + it('rewrites cli install-prefix frames (bin) to a stable app:/// root', () => { + const input = '/usr/local/lib/node_modules/n8n/bin/n8n'; + + expect(normalizeFrameFilename(input)).toBe('app:///bin/n8n'); + }); + + it('prefers the pnpm replacement when both segments are present', () => { + const input = + '/usr/local/lib/node_modules/n8n/node_modules/.pnpm/n8n-core@file+packages+core_abc123/node_modules/n8n-core/src/foo.ts'; + + expect(normalizeFrameFilename(input)).toBe('app:///n8n-core/src/foo.ts'); + }); + + it('leaves unrelated frames unchanged', () => { + const input = '/some/other/path/file.ts'; + + expect(normalizeFrameFilename(input)).toBe('/some/other/path/file.ts'); + }); + + it('leaves node-internal frames unchanged', () => { + const input = 'node:internal/process/task_queues'; + + expect(normalizeFrameFilename(input)).toBe('node:internal/process/task_queues'); + }); + + it('handles pnpm frames not under the cli install prefix (e.g. dev installs)', () => { + const input = + '/home/dev/n8n/node_modules/.pnpm/n8n-core@file+packages+core_abc/node_modules/n8n-core/src/x.ts'; + + expect(normalizeFrameFilename(input)).toBe('app:///n8n-core/src/x.ts'); + }); +}); + describe('ErrorReporter', () => { const errorReporter = new ErrorReporter(mock(), mock()); const event = {} as ErrorEvent; diff --git a/packages/core/src/errors/error-reporter.ts b/packages/core/src/errors/error-reporter.ts index 71e3ff7bd46..756b2c70df6 100644 --- a/packages/core/src/errors/error-reporter.ts +++ b/packages/core/src/errors/error-reporter.ts @@ -56,6 +56,21 @@ const SIX_WEEKS_IN_MS = 6 * 7 * ONE_DAY_IN_MS; const RELEASE_EXPIRATION_WARNING = 'Error tracking disabled because this release is older than 6 weeks.'; +const PNPM_NESTED_FRAME_RE = /.*\/node_modules\/\.pnpm\/[^/]+\/node_modules\//; +const N8N_CLI_INSTALL_PREFIX = '/usr/local/lib/node_modules/n8n/'; + +/** + * Normalises a Sentry stack-frame filename so that pnpm-nested dependency + * paths and the n8n CLI install prefix become stable `app:///` roots. This + * lets Sentry code mappings match `n8n-core`, `n8n-nodes-base`, and cli + * frames without depending on the per-release pnpm peer-deps hash segment. + */ +export function normalizeFrameFilename(filename: string): string { + return filename + .replace(PNPM_NESTED_FRAME_RE, 'app:///') + .replace(N8N_CLI_INSTALL_PREFIX, 'app:///'); +} + @Service() export class ErrorReporter { private expirationTimer?: NodeJS.Timeout; @@ -218,7 +233,15 @@ export class ErrorReporter { ignoreSpans: [`GET ${healthEndpoint}`, 'GET /metrics', 'SET search_path TO'], integrations: (integrations) => [ ...integrations.filter(({ name }) => enabledIntegrations.has(name)), - rewriteFramesIntegration({ root: '/' }), + rewriteFramesIntegration({ + root: '/', + iteratee: (frame) => { + if (frame.filename) { + frame.filename = normalizeFrameFilename(frame.filename); + } + return frame; + }, + }), requestDataIntegration({ include: { cookies: false,