diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89b03fb3e015..17e340c1f207 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,41 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+### Important Changes
+
+- **feat(nextjs): Add Turbopack support for `thirdPartyErrorFilterIntegration` ([#19542](https://github.com/getsentry/sentry-javascript/pull/19542))**
+
+ We added experimental support for the `thirdPartyErrorFilterIntegration` with Turbopack builds.
+
+ This feature requires Next.js 16+ and is currently behind an experimental flag:
+
+ ```js
+ // next.config.ts
+ import { withSentryConfig } from '@sentry/nextjs';
+
+ export default withSentryConfig(nextConfig, {
+ _experimental: {
+ turbopackApplicationKey: 'my-app-key',
+ },
+ });
+ ```
+
+ Then configure the integration in your client instrumentation file with a matching key:
+
+ ```js
+ // instrumentation-client.ts
+ import * as Sentry from '@sentry/nextjs';
+
+ Sentry.init({
+ integrations: [
+ Sentry.thirdPartyErrorFilterIntegration({
+ filterKeys: ['my-app-key'],
+ behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
+ }),
+ ],
+ });
+ ```
+
Work in this release was contributed by @YevheniiKotyrlo. Thank you for your contribution!
## 10.40.0
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx
new file mode 100644
index 000000000000..b6b4bea80def
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import * as Sentry from '@sentry/nextjs';
+
+function throwFirstPartyError(): void {
+ throw new Error('first-party-error');
+}
+
+export default function Page() {
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts
index ae4e3195a2a1..934a50fb786d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts
@@ -7,6 +7,12 @@ Sentry.init({
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
+ integrations: [
+ Sentry.thirdPartyErrorFilterIntegration({
+ filterKeys: ['nextjs-16-e2e'],
+ behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
+ }),
+ ],
// Verify Log type is available
beforeSendLog(log: Log) {
return log;
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 5e72a02200d8..342ba13b1206 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
@@ -10,5 +10,6 @@ export default withSentryConfig(nextConfig, {
silent: true,
_experimental: {
vercelCronsMonitoring: true,
+ turbopackApplicationKey: 'nextjs-16-e2e',
},
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts
new file mode 100644
index 000000000000..277a53fd394b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts
@@ -0,0 +1,25 @@
+import test, { expect } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+const isWebpackDev = process.env.TEST_ENV === 'development-webpack';
+
+test('First-party error should not be tagged as third-party code', async ({ page }) => {
+ test.skip(isWebpackDev, 'Only relevant for Turbopack builds');
+
+ const errorPromise = waitForError('nextjs-16', errorEvent => {
+ return errorEvent?.exception?.values?.some(value => value.value === 'first-party-error') ?? false;
+ });
+
+ await page.goto('/third-party-filter');
+ await page.locator('#first-party-error-btn').click();
+
+ const errorEvent = await errorPromise;
+
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('first-party-error');
+
+ // In production, TEST_ENV=production is shared by both turbopack and webpack variants.
+ // Only assert when the build is actually turbopack.
+ if (errorEvent.tags?.turbopack) {
+ expect(errorEvent.tags?.third_party_code).toBeUndefined();
+ }
+});
diff --git a/packages/nextjs/src/config/loaders/index.ts b/packages/nextjs/src/config/loaders/index.ts
index 322567c1495b..359d72d7def6 100644
--- a/packages/nextjs/src/config/loaders/index.ts
+++ b/packages/nextjs/src/config/loaders/index.ts
@@ -1,3 +1,4 @@
export { default as valueInjectionLoader } from './valueInjectionLoader';
export { default as prefixLoader } from './prefixLoader';
export { default as wrappingLoader } from './wrappingLoader';
+export { default as moduleMetadataInjectionLoader } from './moduleMetadataInjectionLoader';
diff --git a/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts b/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts
new file mode 100644
index 000000000000..3bfe974b59fb
--- /dev/null
+++ b/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts
@@ -0,0 +1,41 @@
+import type { LoaderThis } from './types';
+import { SKIP_COMMENT_AND_DIRECTIVE_REGEX } from './valueInjectionLoader';
+
+export type ModuleMetadataInjectionLoaderOptions = {
+ applicationKey: string;
+};
+
+/**
+ * Inject `_sentryModuleMetadata` into every module so that the
+ * `thirdPartyErrorFilterIntegration` can tell first-party code from
+ * third-party code.
+ *
+ * This is the Turbopack equivalent of what `@sentry/webpack-plugin` does
+ * via its `moduleMetadata` option.
+ *
+ * Options:
+ * - `applicationKey`: The application key used to tag first-party modules.
+ */
+export default function moduleMetadataInjectionLoader(
+ this: LoaderThis,
+ userCode: string,
+): string {
+ const { applicationKey } = 'getOptions' in this ? this.getOptions() : this.query;
+
+ // We do not want to cache injected values across builds
+ this.cacheable(false);
+
+ // The snippet mirrors what @sentry/webpack-plugin injects for moduleMetadata.
+ // We access _sentryModuleMetadata via globalThis (not as a bare variable) to avoid
+ // ReferenceError in strict mode. Each module is keyed by its Error stack trace so that
+ // the SDK can map filenames to metadata at runtime.
+ // Not putting any newlines in the generated code will decrease the likelihood of sourcemaps breaking.
+ const metadata = JSON.stringify({ [`_sentryBundlerPluginAppKey:${applicationKey}`]: true });
+ const injectedCode =
+ ';globalThis._sentryModuleMetadata = globalThis._sentryModuleMetadata || {};' +
+ `globalThis._sentryModuleMetadata[(new Error).stack] = Object.assign({}, globalThis._sentryModuleMetadata[(new Error).stack], ${metadata});`;
+
+ return userCode.replace(SKIP_COMMENT_AND_DIRECTIVE_REGEX, match => {
+ return match + injectedCode;
+ });
+}
diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
index c15413cd1444..3fe15a8e5872 100644
--- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
+++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
@@ -10,7 +10,7 @@ export type ValueInjectionLoaderOptions = {
// We need to be careful not to inject anything before any `"use strict";`s or "use client"s or really any other directive.
// As an additional complication directives may come after any number of comments.
// This regex is shamelessly stolen from: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/7f984482c73e4284e8b12a08dfedf23b5a82f0af/packages/bundler-plugin-core/src/index.ts#L535-L539
-const SKIP_COMMENT_AND_DIRECTIVE_REGEX =
+export const SKIP_COMMENT_AND_DIRECTIVE_REGEX =
// Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files.
new RegExp('^(?:\\s*|/\\*(?:.|\\r|\\n)*?\\*/|//.*[\\n\\r])*(?:"[^"]*";?|\'[^\']*\';?)?');
diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
index 3484a169d018..d8f70efbacf1 100644
--- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
+++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
@@ -1,8 +1,9 @@
import { debug } from '@sentry/core';
+import * as path from 'path';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
-import { supportsNativeDebugIds } from '../util';
+import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';
/**
@@ -56,6 +57,28 @@ export function constructTurbopackConfig({
newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher, rule });
}
+ // Add module metadata injection loader for thirdPartyErrorFilterIntegration support.
+ // This is only added when turbopackApplicationKey is set AND the Next.js version supports the
+ // `condition` field in Turbopack rules (Next.js 16+). Without `condition: { not: 'foreign' }`,
+ // the loader would tag node_modules as first-party, defeating the purpose.
+ const applicationKey = userSentryOptions?._experimental?.turbopackApplicationKey;
+ if (applicationKey && nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion)) {
+ newConfig.rules = safelyAddTurbopackRule(newConfig.rules, {
+ matcher: '*.{ts,tsx,js,jsx,mjs,cjs}',
+ rule: {
+ condition: { not: 'foreign' },
+ loaders: [
+ {
+ loader: path.resolve(__dirname, '..', 'loaders', 'moduleMetadataInjectionLoader.js'),
+ options: {
+ applicationKey,
+ },
+ },
+ ],
+ },
+ });
+ }
+
return newConfig;
}
diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts
index bc2b5463d2ca..233860fb1388 100644
--- a/packages/nextjs/src/config/types.ts
+++ b/packages/nextjs/src/config/types.ts
@@ -713,6 +713,17 @@ export type SentryBuildOptions = {
* Requires cron jobs to be configured in `vercel.json`.
*/
vercelCronsMonitoring?: boolean;
+ /**
+ * Application key used by `thirdPartyErrorFilterIntegration` to distinguish
+ * first-party code from third-party code in Turbopack builds.
+ *
+ * When set, a Turbopack loader injects `_sentryModuleMetadata` into every
+ * first-party module, mirroring what `@sentry/webpack-plugin` does for
+ * webpack builds via its `moduleMetadata` / `applicationKey` option.
+ *
+ * Requires Next.js 16+
+ */
+ turbopackApplicationKey?: string;
}>;
/**
diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts
index 329ca59eedc1..6eecd83905b8 100644
--- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts
+++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts
@@ -902,6 +902,27 @@ describe('getBuildPluginOptions', () => {
});
});
+ describe('applicationKey is not forwarded to webpack plugin', () => {
+ it('does not include turbopackApplicationKey in webpack plugin options', () => {
+ const sentryBuildOptions: SentryBuildOptions = {
+ org: 'test-org',
+ project: 'test-project',
+ _experimental: { turbopackApplicationKey: 'my-app' },
+ };
+
+ const result = getBuildPluginOptions({
+ sentryBuildOptions,
+ releaseName: mockReleaseName,
+ distDirAbsPath: mockDistDirAbsPath,
+ buildTool: 'webpack-client',
+ });
+
+ // turbopackApplicationKey should only be used by the Turbopack loader,
+ // not forwarded to the webpack plugin
+ expect(result.applicationKey).toBeUndefined();
+ });
+ });
+
describe('edge cases', () => {
it('handles undefined release name gracefully', () => {
const sentryBuildOptions: SentryBuildOptions = {
diff --git a/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts b/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts
new file mode 100644
index 000000000000..35e7ba4b692a
--- /dev/null
+++ b/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts
@@ -0,0 +1,129 @@
+import { describe, expect, it } from 'vitest';
+import type { ModuleMetadataInjectionLoaderOptions } from '../../src/config/loaders/moduleMetadataInjectionLoader';
+import moduleMetadataInjectionLoader from '../../src/config/loaders/moduleMetadataInjectionLoader';
+import type { LoaderThis } from '../../src/config/loaders/types';
+
+function createLoaderThis(
+ applicationKey: string,
+ useGetOptions = true,
+): LoaderThis {
+ const base = {
+ addDependency: () => undefined,
+ async: () => undefined,
+ cacheable: () => undefined,
+ callback: () => undefined,
+ resourcePath: './app/page.tsx',
+ };
+
+ if (useGetOptions) {
+ return { ...base, getOptions: () => ({ applicationKey }) } as LoaderThis;
+ }
+
+ return { ...base, query: { applicationKey } } as LoaderThis;
+}
+
+describe('moduleMetadataInjectionLoader', () => {
+ it('should inject metadata snippet into simple code', () => {
+ const loaderThis = createLoaderThis('my-app');
+ const userCode = "import * as Sentry from '@sentry/nextjs';\nSentry.init();";
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toContain('_sentryModuleMetadata');
+ expect(result).toContain('_sentryBundlerPluginAppKey:my-app');
+ expect(result).toContain('Object.assign');
+ });
+
+ it('should inject after "use strict" directive', () => {
+ const loaderThis = createLoaderThis('my-app');
+ const userCode = '"use strict";\nconsole.log("hello");';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ const metadataIndex = result.indexOf('_sentryModuleMetadata');
+ const directiveIndex = result.indexOf('"use strict"');
+ expect(metadataIndex).toBeGreaterThan(directiveIndex);
+ });
+
+ it('should inject after "use client" directive', () => {
+ const loaderThis = createLoaderThis('my-app');
+ const userCode = '"use client";\nimport React from \'react\';';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ const metadataIndex = result.indexOf('_sentryModuleMetadata');
+ const directiveIndex = result.indexOf('"use client"');
+ expect(metadataIndex).toBeGreaterThan(directiveIndex);
+ });
+
+ it('should handle code with leading comments before directives', () => {
+ const loaderThis = createLoaderThis('my-app');
+ const userCode = '// some comment\n"use client";\nimport React from \'react\';';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toContain('_sentryBundlerPluginAppKey:my-app');
+ const metadataIndex = result.indexOf('_sentryModuleMetadata');
+ const directiveIndex = result.indexOf('"use client"');
+ expect(metadataIndex).toBeGreaterThan(directiveIndex);
+ });
+
+ it('should handle code with block comments before directives', () => {
+ const loaderThis = createLoaderThis('my-app');
+ const userCode = '/* block comment */\n"use client";\nimport React from \'react\';';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toContain('_sentryBundlerPluginAppKey:my-app');
+ });
+
+ it('should set cacheable to false', () => {
+ let cacheableValue: boolean | undefined;
+ const loaderThis = {
+ addDependency: () => undefined,
+ async: () => undefined,
+ cacheable: (flag: boolean) => {
+ cacheableValue = flag;
+ },
+ callback: () => undefined,
+ resourcePath: './app/page.tsx',
+ getOptions: () => ({ applicationKey: 'my-app' }),
+ } as LoaderThis;
+
+ moduleMetadataInjectionLoader.call(loaderThis, 'const x = 1;');
+
+ expect(cacheableValue).toBe(false);
+ });
+
+ it('should work with webpack 4 query API', () => {
+ const loaderThis = createLoaderThis('my-app', false);
+ const userCode = 'const x = 1;';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toContain('_sentryBundlerPluginAppKey:my-app');
+ });
+
+ it('should use globalThis and Object.assign merge pattern keyed by stack trace', () => {
+ const loaderThis = createLoaderThis('my-app');
+ const userCode = 'const x = 1;';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ // Should use globalThis to avoid ReferenceError in strict mode
+ expect(result).toContain('globalThis._sentryModuleMetadata = globalThis._sentryModuleMetadata || {}');
+ // Should key by stack trace like the webpack plugin does
+ expect(result).toContain('globalThis._sentryModuleMetadata[(new Error).stack]');
+ // Should use Object.assign to merge metadata
+ expect(result).toContain('Object.assign({}');
+ });
+
+ it('should contain the correct app key format in output', () => {
+ const loaderThis = createLoaderThis('test-key-123');
+ const userCode = 'export default function Page() {}';
+
+ const result = moduleMetadataInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toContain('"_sentryBundlerPluginAppKey:test-key-123":true');
+ });
+});
diff --git a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts
index 00bf3ece5935..d1bf313d16f2 100644
--- a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts
+++ b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts
@@ -12,7 +12,13 @@ vi.mock('path', async () => {
const actual = await vi.importActual('path');
return {
...actual,
- resolve: vi.fn().mockReturnValue('/mocked/path/to/valueInjectionLoader.js'),
+ resolve: vi.fn().mockImplementation((...args: string[]) => {
+ const lastArg = args[args.length - 1];
+ if (lastArg === 'moduleMetadataInjectionLoader.js') {
+ return '/mocked/path/to/moduleMetadataInjectionLoader.js';
+ }
+ return '/mocked/path/to/valueInjectionLoader.js';
+ }),
};
});
@@ -936,6 +942,144 @@ describe('condition field version gating', () => {
});
});
+describe('moduleMetadataInjection with applicationKey', () => {
+ it('should add metadata loader rule when applicationKey is set and Next.js >= 16', () => {
+ const pathResolveSpy = vi.spyOn(path, 'resolve');
+ pathResolveSpy.mockImplementation((...args: string[]) => {
+ const lastArg = args[args.length - 1];
+ if (lastArg === 'moduleMetadataInjectionLoader.js') {
+ return '/mocked/path/to/moduleMetadataInjectionLoader.js';
+ }
+ return '/mocked/path/to/valueInjectionLoader.js';
+ });
+
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: { _experimental: { turbopackApplicationKey: 'my-app' } },
+ nextJsVersion: '16.0.0',
+ });
+
+ expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toEqual({
+ condition: { not: 'foreign' },
+ loaders: [
+ {
+ loader: '/mocked/path/to/moduleMetadataInjectionLoader.js',
+ options: {
+ applicationKey: 'my-app',
+ },
+ },
+ ],
+ });
+ });
+
+ it('should NOT add metadata loader rule when Next.js < 16', () => {
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: { _experimental: { turbopackApplicationKey: 'my-app' } },
+ nextJsVersion: '15.4.1',
+ });
+
+ expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeUndefined();
+ });
+
+ it('should NOT add metadata loader rule when applicationKey is not set', () => {
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: {},
+ nextJsVersion: '16.0.0',
+ });
+
+ expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeUndefined();
+ });
+
+ it('should NOT add metadata loader rule when nextJsVersion is undefined', () => {
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: { _experimental: { turbopackApplicationKey: 'my-app' } },
+ nextJsVersion: undefined,
+ });
+
+ expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeUndefined();
+ });
+
+ it('should pass applicationKey through to loader options correctly', () => {
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: { _experimental: { turbopackApplicationKey: 'custom-key-123' } },
+ nextJsVersion: '16.0.0',
+ });
+
+ const rule = result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}'] as {
+ condition: unknown;
+ loaders: Array<{ loader: string; options: { applicationKey: string } }>;
+ };
+ expect(rule.loaders[0]!.options.applicationKey).toBe('custom-key-123');
+ });
+
+ it('should coexist with existing value injection rules', () => {
+ const userNextConfig: NextConfigObject = {};
+ const mockRouteManifest: RouteManifest = {
+ dynamicRoutes: [],
+ staticRoutes: [{ path: '/', regex: '/' }],
+ isrRoutes: [],
+ };
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: { _experimental: { turbopackApplicationKey: 'my-app' } },
+ routeManifest: mockRouteManifest,
+ nextJsVersion: '16.0.0',
+ });
+
+ // Value injection rules should still be present
+ expect(result.rules!['**/instrumentation-client.*']).toBeDefined();
+ expect(result.rules!['**/instrumentation.*']).toBeDefined();
+ // Metadata loader rule should also be present
+ expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeDefined();
+ });
+
+ it('should add metadata loader rule for Next.js 17+', () => {
+ const pathResolveSpy = vi.spyOn(path, 'resolve');
+ pathResolveSpy.mockImplementation((...args: string[]) => {
+ const lastArg = args[args.length - 1];
+ if (lastArg === 'moduleMetadataInjectionLoader.js') {
+ return '/mocked/path/to/moduleMetadataInjectionLoader.js';
+ }
+ return '/mocked/path/to/valueInjectionLoader.js';
+ });
+
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ userSentryOptions: { _experimental: { turbopackApplicationKey: 'my-app' } },
+ nextJsVersion: '17.0.0',
+ });
+
+ expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toEqual({
+ condition: { not: 'foreign' },
+ loaders: [
+ {
+ loader: '/mocked/path/to/moduleMetadataInjectionLoader.js',
+ options: {
+ applicationKey: 'my-app',
+ },
+ },
+ ],
+ });
+ });
+});
+
describe('safelyAddTurbopackRule', () => {
const mockRule = {
loaders: [