Skip to content

Commit d820774

Browse files
committed
feat: added warning if the user hasn't configured the matcher properly
1 parent e4dd992 commit d820774

File tree

3 files changed

+214
-1
lines changed

3 files changed

+214
-1
lines changed

packages/nextjs/src/config/util.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,37 @@ export function detectActiveBundler(): 'turbopack' | 'webpack' {
181181
return 'webpack';
182182
}
183183
}
184+
185+
/**
186+
* Finds the middleware or proxy file in the Next.js project.
187+
* Next.js only allows one middleware file, so this returns the first match.
188+
*/
189+
export function findMiddlewareFile(): { path: string; contents: string } | undefined {
190+
const projectDir = process.cwd();
191+
192+
// In Next.js 16+, the file is called 'proxy', in earlier versions it's 'middleware'
193+
const nextVersion = getNextjsVersion();
194+
const nextMajor = nextVersion ? parseSemver(nextVersion).major : undefined;
195+
const basename = nextMajor && nextMajor >= 16 ? 'proxy' : 'middleware';
196+
const directories = [projectDir, `${projectDir}/src`];
197+
const extensions = ['.ts', '.js'];
198+
199+
// Find the first existing middleware/proxy file
200+
for (const dir of directories) {
201+
for (const ext of extensions) {
202+
const filePath = `${dir}/${basename}${ext}`;
203+
if (fs.existsSync(filePath)) {
204+
try {
205+
const contents = fs.readFileSync(filePath, 'utf-8');
206+
207+
return { path: filePath, contents };
208+
} catch {
209+
// If we can't read the file, continue searching
210+
continue;
211+
}
212+
}
213+
}
214+
}
215+
216+
return undefined;
217+
}

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
} from './types';
1919
import {
2020
detectActiveBundler,
21+
findMiddlewareFile,
2122
getNextjsVersion,
2223
requiresInstrumentationHook,
2324
supportsProductionCompileHook,
@@ -26,6 +27,7 @@ import { constructWebpackConfigFunction } from './webpack';
2627

2728
let showedExportModeTunnelWarning = false;
2829
let showedExperimentalBuildModeWarning = false;
30+
let showedMiddlewareMatcherWarning = false;
2931

3032
// Packages we auto-instrument need to be external for instrumentation to work
3133
// Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages
@@ -89,6 +91,50 @@ export function withSentryConfig<C>(nextConfig?: C, sentryBuildOptions: SentryBu
8991
}
9092
}
9193

94+
/**
95+
* Checks if the user has a middleware/proxy file with a matcher that might exclude the tunnel route.
96+
* Warns the user if they have a matcher but are not using withSentryTunnelExclusion.
97+
*/
98+
function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void {
99+
if (showedMiddlewareMatcherWarning) {
100+
return;
101+
}
102+
103+
try {
104+
const middlewareFile = findMiddlewareFile();
105+
106+
// No middleware file found
107+
if (!middlewareFile) {
108+
return;
109+
}
110+
111+
// Check if they're already using withSentryTunnelExclusion
112+
if (middlewareFile.contents.includes('withSentryTunnelExclusion')) {
113+
return;
114+
}
115+
116+
// Look for config.matcher export
117+
const isProxy = middlewareFile.path.includes('proxy');
118+
const hasConfigMatcher = /export\s+const\s+config\s*=\s*{[^}]*matcher\s*:/s.test(middlewareFile.contents);
119+
120+
if (hasConfigMatcher) {
121+
// eslint-disable-next-line no-console
122+
console.warn(
123+
`[@sentry/nextjs] WARNING: You have a ${isProxy ? 'proxy' : 'middleware'} file (${path.basename(middlewareFile.path)}) with a \`config.matcher\`. ` +
124+
`If your matcher does not include the Sentry tunnel route (${tunnelPath}), tunnel requests may be blocked. ` +
125+
"To ensure your matcher doesn't interfere with Sentry event delivery, wrap your matcher with `withSentryTunnelExclusion`:\n\n" +
126+
" import { withSentryTunnelExclusion } from '@sentry/nextjs';\n" +
127+
' export const config = {\n' +
128+
" matcher: withSentryTunnelExclusion(['/your/routes']),\n" +
129+
' };\n',
130+
);
131+
showedMiddlewareMatcherWarning = true;
132+
}
133+
} catch {
134+
// Silently fail - this is just a helpful warning, not critical
135+
}
136+
}
137+
92138
/**
93139
* Generates a random tunnel route path that's less likely to be blocked by ad-blockers
94140
*/
@@ -126,6 +172,7 @@ function getFinalConfigObject(
126172
userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined;
127173

128174
setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute);
175+
checkMiddlewareMatcherForTunnelRoute(resolvedTunnelRoute);
129176
}
130177
}
131178

packages/nextjs/test/config/util.test.ts

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
24
import * as util from '../../src/config/util';
35

46
describe('util', () => {
@@ -334,4 +336,134 @@ describe('util', () => {
334336
expect(util.detectActiveBundler()).toBe('webpack');
335337
});
336338
});
339+
340+
describe('findMiddlewareFile', () => {
341+
vi.mock('../../src/config/util', async () => {
342+
const actual = await vi.importActual('../../src/config/util');
343+
return {
344+
...actual,
345+
getNextjsVersion: vi.fn(),
346+
};
347+
});
348+
349+
let originalCwd: string;
350+
let testDir: string;
351+
352+
beforeEach(() => {
353+
originalCwd = process.cwd();
354+
testDir = path.join(__dirname, '.test-middleware-temp');
355+
356+
// Create test directory
357+
if (!fs.existsSync(testDir)) {
358+
fs.mkdirSync(testDir, { recursive: true });
359+
}
360+
361+
process.chdir(testDir);
362+
});
363+
364+
afterEach(() => {
365+
process.chdir(originalCwd);
366+
367+
// Clean up test directory
368+
if (fs.existsSync(testDir)) {
369+
fs.rmSync(testDir, { recursive: true, force: true });
370+
}
371+
372+
vi.clearAllMocks();
373+
});
374+
375+
describe('Next.js <16 (middleware)', () => {
376+
beforeEach(() => {
377+
vi.mocked(util.getNextjsVersion).mockReturnValue('15.0.0');
378+
});
379+
380+
it('should find middleware.ts in root directory', () => {
381+
fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'export default function middleware() {}');
382+
383+
const result = util.findMiddlewareFile();
384+
385+
expect(result).toBeDefined();
386+
expect(result?.path).toContain('middleware.ts');
387+
expect(result?.contents).toBe('export default function middleware() {}');
388+
});
389+
390+
it('should find middleware.js in root directory', () => {
391+
fs.writeFileSync(path.join(testDir, 'middleware.js'), 'module.exports = function middleware() {}');
392+
393+
const result = util.findMiddlewareFile();
394+
395+
expect(result).toBeDefined();
396+
expect(result?.path).toContain('middleware.js');
397+
expect(result?.contents).toBe('module.exports = function middleware() {}');
398+
});
399+
400+
it('should find middleware.ts in src directory', () => {
401+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
402+
fs.writeFileSync(path.join(testDir, 'src', 'middleware.ts'), 'export default function middleware() {}');
403+
404+
const result = util.findMiddlewareFile();
405+
406+
expect(result).toBeDefined();
407+
expect(result?.path).toContain(path.join('src', 'middleware.ts'));
408+
expect(result?.contents).toBe('export default function middleware() {}');
409+
});
410+
411+
it('should prefer root over src directory', () => {
412+
fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
413+
fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'root middleware');
414+
fs.writeFileSync(path.join(testDir, 'src', 'middleware.ts'), 'src middleware');
415+
416+
const result = util.findMiddlewareFile();
417+
418+
expect(result).toBeDefined();
419+
expect(result?.contents).toBe('root middleware');
420+
});
421+
422+
it('should prefer .ts over .js extension', () => {
423+
fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'typescript middleware');
424+
fs.writeFileSync(path.join(testDir, 'middleware.js'), 'javascript middleware');
425+
426+
const result = util.findMiddlewareFile();
427+
428+
expect(result).toBeDefined();
429+
expect(result?.contents).toBe('typescript middleware');
430+
});
431+
432+
it('should NOT find proxy.ts on Next.js <16', () => {
433+
fs.writeFileSync(path.join(testDir, 'proxy.ts'), 'export default function proxy() {}');
434+
435+
const result = util.findMiddlewareFile();
436+
437+
expect(result).toBeUndefined();
438+
});
439+
440+
it('should return undefined when no middleware file exists', () => {
441+
const result = util.findMiddlewareFile();
442+
443+
expect(result).toBeUndefined();
444+
});
445+
});
446+
447+
// Note: Tests for Next.js 16+ (proxy) behavior would require mocking getNextjsVersion,
448+
// which is challenging in this test setup due to module imports.
449+
// The logic is the same as middleware tests but with 'proxy' instead of 'middleware'.
450+
451+
describe('edge cases', () => {
452+
beforeEach(() => {
453+
vi.mocked(util.getNextjsVersion).mockReturnValue('15.0.0');
454+
});
455+
456+
it('should handle when getNextjsVersion returns undefined', () => {
457+
vi.mocked(util.getNextjsVersion).mockReturnValue(undefined);
458+
459+
fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'export default function middleware() {}');
460+
461+
const result = util.findMiddlewareFile();
462+
463+
// Should default to 'middleware' when version is unknown
464+
expect(result).toBeDefined();
465+
expect(result?.path).toContain('middleware.ts');
466+
});
467+
});
468+
});
337469
});

0 commit comments

Comments
 (0)