fix(core): Normalise Sentry stack frame paths to stable app:/// roots (#31421)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
n8n-cat-bot[bot]
2026-05-30 19:16:31 +00:00
committed by GitHub
co-authored by n8n-cat-bot[bot] Claude Opus 4.7
parent d6eb844c32
commit e620545c93
2 changed files with 92 additions and 2 deletions
@@ -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;
+24 -1
View File
@@ -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,