diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/sri-test/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/sri-test/page.tsx
new file mode 100644
index 000000000000..94e17a8bb4ef
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/sri-test/page.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import Link from 'next/link';
+import { useState } from 'react';
+
+export default function SriTestPage() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
SRI Test Page
+
+
+ Go to target
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/sri-test/target/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/sri-test/target/page.tsx
new file mode 100644
index 000000000000..80ea89c506d5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/sri-test/target/page.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import Link from 'next/link';
+import { useState } from 'react';
+
+export default function SriTestTargetPage() {
+ const [clicked, setClicked] = useState(false);
+
+ return (
+
+
SRI Target Page
+
+
+ Go back
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts
index 41814b8152d0..ee93730e8d1d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts
@@ -4,7 +4,13 @@ import type { NextConfig } from 'next';
// Simulate Vercel environment for cron monitoring tests
process.env.VERCEL = '1';
-const nextConfig: NextConfig = {};
+const nextConfig: NextConfig = {
+ experimental: {
+ sri: {
+ algorithm: 'sha256',
+ },
+ },
+};
export default withSentryConfig(nextConfig, {
silent: true,
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/sri.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/sri.test.ts
new file mode 100644
index 000000000000..c68d23c21de6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/sri.test.ts
@@ -0,0 +1,60 @@
+import { expect, test } from '@playwright/test';
+
+const isDevMode = !!process.env.TEST_ENV && process.env.TEST_ENV.includes('development');
+
+test.describe('Subresource Integrity (SRI)', () => {
+ test('page with client components loads correctly with SRI enabled', async ({ page }) => {
+ // SRI is only relevant for production builds
+ test.skip(isDevMode, 'SRI only applies to production builds');
+
+ const consoleErrors: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'error') {
+ consoleErrors.push(msg.text());
+ }
+ });
+
+ await page.goto('/sri-test');
+
+ const heading = page.locator('#sri-test-heading');
+ await expect(heading).toBeVisible();
+
+ // Verify client-side interactivity works (scripts loaded correctly)
+ const button = page.locator('#counter-button');
+ await expect(button).toContainText('Count: 0');
+ await button.click();
+ await expect(button).toContainText('Count: 1');
+
+ expect(consoleErrors.filter(e => e.includes('integrity'))).toHaveLength(0);
+ });
+
+ test('client-side navigation works with SRI enabled', async ({ page }) => {
+ test.skip(isDevMode, 'SRI only applies to production builds');
+
+ const consoleErrors: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'error') {
+ consoleErrors.push(msg.text());
+ }
+ });
+
+ await page.goto('/sri-test');
+ await expect(page.locator('#sri-test-heading')).toBeVisible();
+
+ // Navigate to target page via client-side link
+ await page.locator('#navigate-link').click();
+ await expect(page.locator('#sri-target-heading')).toBeVisible();
+
+ // Verify client-side interactivity on the target page
+ const targetButton = page.locator('#target-button');
+ await expect(targetButton).toContainText('Click me');
+ await targetButton.click();
+ await expect(targetButton).toContainText('Clicked!');
+
+ // Navigate back
+ await page.locator('#back-link').click();
+ await expect(page.locator('#sri-test-heading')).toBeVisible();
+
+ expect(consoleErrors.filter(e => e.includes('integrity'))).toHaveLength(0);
+ });
+});
diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts
index f6e5a3b21617..25e4d1a6a485 100644
--- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts
+++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts
@@ -15,7 +15,14 @@ export async function handleRunAfterProductionCompile(
distDir,
buildTool,
usesNativeDebugIds,
- }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack'; usesNativeDebugIds?: boolean },
+ sriEnabled,
+ }: {
+ releaseName?: string;
+ distDir: string;
+ buildTool: 'webpack' | 'turbopack';
+ usesNativeDebugIds?: boolean;
+ sriEnabled?: boolean;
+ },
sentryBuildOptions: SentryBuildOptions,
): Promise {
if (sentryBuildOptions.debug) {
@@ -68,10 +75,19 @@ export async function handleRunAfterProductionCompile(
// the deleted .map files, and in Next.js 16 (turbopack) those requests fall through
// to the app router instead of returning 404, which can break middleware-dependent
// features like Clerk auth.
+ // When SRI is enabled, we must skip this step because Next.js computes integrity
+ // hashes during the build — modifying files afterward invalidates those hashes.
const deleteSourcemapsAfterUpload = sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload ?? false;
- if (deleteSourcemapsAfterUpload && buildTool === 'turbopack') {
+ if (deleteSourcemapsAfterUpload && buildTool === 'turbopack' && !sriEnabled) {
await stripSourceMappingURLComments(path.join(distDir, 'static'), sentryBuildOptions.debug);
}
+
+ if (deleteSourcemapsAfterUpload && buildTool === 'turbopack' && sriEnabled && sentryBuildOptions.debug) {
+ // eslint-disable-next-line no-console
+ console.debug(
+ '[@sentry/nextjs] Skipping sourceMappingURL comment stripping because Subresource Integrity (SRI) is enabled.',
+ );
+ }
}
const SOURCEMAPPING_URL_COMMENT_REGEX = /\n?\/\/[#@] sourceMappingURL=[^\n]+$/;
diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts
index 9aa31f79e535..86068841e773 100644
--- a/packages/nextjs/src/config/types.ts
+++ b/packages/nextjs/src/config/types.ts
@@ -46,6 +46,7 @@ export type NextConfigObject = {
instrumentationHook?: boolean;
clientTraceMetadata?: string[];
serverComponentsExternalPackages?: string[]; // next < v15.0.0
+ sri?: { algorithm?: string };
};
productionBrowserSourceMaps?: boolean;
// https://nextjs.org/docs/pages/api-reference/next-config-js/env
diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts
index 76187ca319f9..edd62b8ba8c3 100644
--- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts
+++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts
@@ -140,6 +140,7 @@ export function maybeSetUpRunAfterProductionCompileHook({
distDir,
buildTool: bundlerInfo.isTurbopack ? 'turbopack' : 'webpack',
usesNativeDebugIds: bundlerInfo.isTurbopack ? turboPackConfig?.debugIds : undefined,
+ sriEnabled: !!incomingUserNextConfigObject.experimental?.sri,
},
userSentryOptions,
);
@@ -160,6 +161,7 @@ export function maybeSetUpRunAfterProductionCompileHook({
distDir,
buildTool: bundlerInfo.isTurbopack ? 'turbopack' : 'webpack',
usesNativeDebugIds: bundlerInfo.isTurbopack ? turboPackConfig?.debugIds : undefined,
+ sriEnabled: !!incomingUserNextConfigObject.experimental?.sri,
},
userSentryOptions,
);
diff --git a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts
index 3d551dfb6c40..7c21c26d2ef6 100644
--- a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts
+++ b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts
@@ -388,6 +388,43 @@ describe('handleRunAfterProductionCompile', () => {
expect(readdirSpy).not.toHaveBeenCalled();
});
+
+ it('does NOT strip sourceMappingURL comments when SRI is enabled', async () => {
+ await handleRunAfterProductionCompile(
+ {
+ releaseName: 'test-release',
+ distDir: '/path/to/.next',
+ buildTool: 'turbopack',
+ sriEnabled: true,
+ },
+ {
+ ...mockSentryBuildOptions,
+ sourcemaps: { deleteSourcemapsAfterUpload: true },
+ },
+ );
+
+ expect(readdirSpy).not.toHaveBeenCalled();
+ });
+
+ it('strips sourceMappingURL comments when SRI is not enabled', async () => {
+ await handleRunAfterProductionCompile(
+ {
+ releaseName: 'test-release',
+ distDir: '/path/to/.next',
+ buildTool: 'turbopack',
+ sriEnabled: false,
+ },
+ {
+ ...mockSentryBuildOptions,
+ sourcemaps: { deleteSourcemapsAfterUpload: true },
+ },
+ );
+
+ expect(readdirSpy).toHaveBeenCalledWith(
+ path.join('/path/to/.next', 'static'),
+ expect.objectContaining({ recursive: true }),
+ );
+ });
});
describe('path handling', () => {