diff --git a/sdk/highlight-next/src/util/with-highlight-config.test.ts b/sdk/highlight-next/src/util/with-highlight-config.test.ts index 6e4c947687..915701c252 100644 --- a/sdk/highlight-next/src/util/with-highlight-config.test.ts +++ b/sdk/highlight-next/src/util/with-highlight-config.test.ts @@ -117,4 +117,56 @@ describe('withHighlightConfig', () => { ).rewrites!(), ).toMatchObject(defaultRewrite) }) + + it('adds OpenTelemetry packages to serverExternalPackages', async () => { + const result = await withHighlightConfig({}) + expect(result.serverExternalPackages).toEqual( + expect.arrayContaining([ + '@highlight-run/node', + '@opentelemetry/instrumentation', + '@prisma/instrumentation', + 'require-in-the-middle', + 'import-in-the-middle', + ]), + ) + }) + + it('preserves user-provided serverExternalPackages and deduplicates', async () => { + const result = await withHighlightConfig({ + serverExternalPackages: ['pino', '@opentelemetry/instrumentation'], + }) + expect(result.serverExternalPackages).toEqual( + expect.arrayContaining([ + 'pino', + '@opentelemetry/instrumentation', + '@highlight-run/node', + ]), + ) + expect( + (result.serverExternalPackages ?? []).filter( + (p) => p === '@opentelemetry/instrumentation', + ), + ).toHaveLength(1) + }) + + it('adds ignoreWarnings for OpenTelemetry on the server build', async () => { + const result = await withHighlightConfig({}) + const webpack = result.webpack as (cfg: any, opts: any) => any + const cfg = webpack({ plugins: [] }, { isServer: true } as any) + expect(cfg.ignoreWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + message: expect.any(RegExp), + module: expect.any(RegExp), + }), + ]), + ) + }) + + it('does not add ignoreWarnings on the client build', async () => { + const result = await withHighlightConfig({}) + const webpack = result.webpack as (cfg: any, opts: any) => any + const cfg = webpack({ plugins: [] }, { isServer: false } as any) + expect(cfg.ignoreWarnings).toBeUndefined() + }) }) diff --git a/sdk/highlight-next/src/util/with-highlight-config.ts b/sdk/highlight-next/src/util/with-highlight-config.ts index 6f6abcd1ee..379b6d4a3d 100644 --- a/sdk/highlight-next/src/util/with-highlight-config.ts +++ b/sdk/highlight-next/src/util/with-highlight-config.ts @@ -3,6 +3,21 @@ import { Rewrite } from 'next/dist/lib/load-custom-routes' import { WebpackConfigContext } from 'next/dist/server/config-shared' import HighlightWebpackPlugin from './highlight-webpack-plugin.js' +// Packages that use require-in-the-middle / import-in-the-middle, which +// webpack cannot statically analyze. Marking them external tells Next to load +// them via Node's runtime require instead of bundling them, which silences the +// "Critical dependency" warnings during `next build`. +const OTEL_SERVER_EXTERNAL_PACKAGES = [ + '@highlight-run/node', + '@opentelemetry/api', + '@opentelemetry/auto-instrumentations-node', + '@opentelemetry/instrumentation', + '@opentelemetry/sdk-node', + '@prisma/instrumentation', + 'import-in-the-middle', + 'require-in-the-middle', +] + interface HighlightConfigOptionsDefault { uploadSourceMaps: boolean configureHighlightProxy: boolean @@ -180,17 +195,39 @@ const getHighlightConfig = async ( } } - let newWebpack = config.webpack - if (defaultOpts.uploadSourceMaps) { - newWebpack = (webpackConfig: any, opts: WebpackConfigContext) => { - let originalConfig = webpackConfig - if (config.webpack) { - originalConfig = config.webpack(webpackConfig, opts) - } - if (opts.isServer) { + const newWebpack = (webpackConfig: any, opts: WebpackConfigContext) => { + let originalConfig = webpackConfig + if (config.webpack) { + originalConfig = config.webpack(webpackConfig, opts) + } + + if (opts.isServer) { + if (defaultOpts.uploadSourceMaps) { originalConfig.devtool = 'source-map' } + // Defense-in-depth: even with serverExternalPackages set, some + // builds (custom resolvers, monorepo hoisting) can still pull + // these modules through webpack. Silence the known-noisy + // warnings rather than letting them spam the customer build. + const criticalDepMessage = + /Critical dependency: the request of a dependency is an expression/ + originalConfig.ignoreWarnings = [ + ...(originalConfig.ignoreWarnings ?? []), + { + module: /node_modules\/(@opentelemetry\/instrumentation|require-in-the-middle|import-in-the-middle|@highlight-run\/node|@prisma\/instrumentation)/, + message: criticalDepMessage, + }, + { + // Workspace / hoisted builds where @highlight-run/node + // is symlinked outside node_modules. + module: /highlight-node\/dist/, + message: criticalDepMessage, + }, + ] + } + + if (defaultOpts.uploadSourceMaps) { originalConfig.plugins.push( new HighlightWebpackPlugin( defaultOpts.apiKey, @@ -200,11 +237,18 @@ const getHighlightConfig = async ( defaultOpts.sourceMapsBackendUrl, ), ) - - return originalConfig } + + return originalConfig } + const existingServerExternal = Array.isArray(config.serverExternalPackages) + ? config.serverExternalPackages + : [] + const serverExternalPackages = Array.from( + new Set([...existingServerExternal, ...OTEL_SERVER_EXTERNAL_PACKAGES]), + ) + return { ...config, env: { @@ -217,6 +261,7 @@ const getHighlightConfig = async ( defaultOpts.uploadSourceMaps || config.productionBrowserSourceMaps, rewrites: newRewrites, webpack: newWebpack, + serverExternalPackages, } }