-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(server-utils): Warn when bundler config has instrumented module in external #22379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,25 @@ | ||
| import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/esbuild'; | ||
| import { escapeStringForRegex } from '@sentry/core'; | ||
| import { instrumentedModuleNames } from '../config'; | ||
| import type { PluginOptions } from './options'; | ||
| import { orchestrionTransformOptions } from './options'; | ||
| import { externalEntryMatchesModule, externalizedModulesWarning, orchestrionTransformOptions } from './options'; | ||
|
|
||
| // esbuild `external` entries may contain `*` wildcards. | ||
| function matchesEsbuildExternal(entry: string, moduleName: string): boolean { | ||
| if (entry.includes('*')) { | ||
| return new RegExp(`^${entry.split('*').map(escapeStringForRegex).join('.*')}$`).test(moduleName); | ||
| } | ||
| return externalEntryMatchesModule(entry, moduleName); | ||
| } | ||
|
|
||
| /** | ||
| * esbuild plugin that runs the orchestrion code transform on the bundled output. | ||
| * | ||
| * Use when bundling a Node app with esbuild. For unbundled Node processes use the | ||
| * runtime hook instead (`node --import @sentry/node/orchestrion app.js`). | ||
| * | ||
| * esbuild does not flatten nested `plugins` arrays, so this returns a single | ||
| * plugin that strips instrumented packages from an `external` denylist before | ||
| * delegating to the upstream transform. | ||
| * Instrumented packages marked as `external` never pass through the code | ||
| * transform, so a build warning is emitted for them. | ||
| * | ||
| * @example | ||
| * ```ts | ||
|
|
@@ -20,5 +29,21 @@ import { orchestrionTransformOptions } from './options'; | |
| * ``` | ||
| */ | ||
| export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType<typeof codeTransformer> { | ||
| return codeTransformer(orchestrionTransformOptions(options)); | ||
| const plugin = codeTransformer(orchestrionTransformOptions(options)); | ||
| const moduleNames = instrumentedModuleNames(options.instrumentations); | ||
| const setup = plugin.setup; | ||
|
|
||
| return { | ||
| ...plugin, | ||
| setup(build): ReturnType<typeof setup> { | ||
| const external = build.initialOptions.external || []; | ||
| const externalizedModules = moduleNames.filter(name => | ||
| external.some(entry => matchesEsbuildExternal(entry, name)), | ||
| ); | ||
| if (externalizedModules.length > 0) { | ||
| build.onStart(() => ({ warnings: [{ text: externalizedModulesWarning(externalizedModules) }] })); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Esbuild misses packages external optionMedium Severity The new esbuild warning only inspects Reviewed by Cursor Bugbot for commit b496d7c. Configure here. |
||
| return setup(build); | ||
| }, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/rollup'; | ||
| import type { NormalizedInputOptions, PluginContext } from 'rollup'; | ||
| import { instrumentedModuleNames } from '../config'; | ||
| import type { PluginOptions } from './options'; | ||
| import { orchestrionTransformOptions } from './options'; | ||
| import { externalizedModulesWarning, orchestrionTransformOptions } from './options'; | ||
|
|
||
| /** | ||
| * Rollup plugin that runs the orchestrion code transform on the bundled output. | ||
|
|
@@ -16,5 +18,19 @@ import { orchestrionTransformOptions } from './options'; | |
| * ``` | ||
| */ | ||
| export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType<typeof codeTransformer> { | ||
| return codeTransformer(orchestrionTransformOptions(options)); | ||
| const moduleNames = instrumentedModuleNames(options.instrumentations); | ||
|
|
||
| return { | ||
| ...codeTransformer(orchestrionTransformOptions(options)), | ||
| buildStart(this: PluginContext, rollupOptions: NormalizedInputOptions): void { | ||
| // An externalized dependency never passes through the code transform, so | ||
| // its diagnostics_channel calls are silently never injected. By the time | ||
| // buildStart runs, Rollup has normalized `external` (string arrays, | ||
| // RegExps or user functions) into a single predicate we can probe. | ||
| const externalizedModules = moduleNames.filter(name => rollupOptions.external(name, undefined, false)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I didn't realize it did that. Very convenient. |
||
| if (externalizedModules.length > 0) { | ||
| this.warn(externalizedModulesWarning(externalizedModules)); | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,140 @@ | ||
| import { existsSync } from 'node:fs'; | ||
| import { join } from 'node:path'; | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { getTracingHooksDirectory } from '../../src/orchestrion/bundler/webpack'; | ||
| import type { OnStartResult, PluginBuild } from 'esbuild'; | ||
| import type { NormalizedInputOptions, PluginContext } from 'rollup'; | ||
| import type { ResolvedConfig } from 'vite'; | ||
| import type { Compiler } from 'webpack'; | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
| import { sentryOrchestrionPlugin as esbuildPlugin } from '../../src/orchestrion/bundler/esbuild'; | ||
| import { sentryOrchestrionPlugin as rollupPlugin } from '../../src/orchestrion/bundler/rollup'; | ||
| import { sentryOrchestrionPlugin as vitePlugin } from '../../src/orchestrion/bundler/vite'; | ||
| import { getTracingHooksDirectory, sentryOrchestrionWebpackPlugin } from '../../src/orchestrion/bundler/webpack'; | ||
|
|
||
| // The upstream transform plugins are mocked so tests exercise only the hooks | ||
| // added on top of them (the externalized-modules warnings). | ||
| vi.mock('@apm-js-collab/code-transformer-bundler-plugins/esbuild', () => ({ | ||
| default: () => ({ name: 'code-transformer', setup: vi.fn() }), | ||
| })); | ||
| vi.mock('@apm-js-collab/code-transformer-bundler-plugins/vite', () => ({ | ||
| default: () => ({ name: 'code-transformer' }), | ||
| })); | ||
| vi.mock('@apm-js-collab/code-transformer-bundler-plugins/webpack', () => ({ | ||
| default: () => ({ apply: vi.fn() }), | ||
| })); | ||
|
|
||
| describe('sentryOrchestrionPlugin (rollup)', () => { | ||
| // Mirrors what Rollup passes to buildStart: `external` is already normalized | ||
| // into a predicate function, regardless of how the user configured it. | ||
| function runBuildStart(external: (source: string) => boolean): ReturnType<typeof vi.fn> { | ||
| const warn = vi.fn(); | ||
| const plugin = rollupPlugin(); | ||
| (plugin.buildStart as (this: unknown, options: unknown) => void).call( | ||
| { warn } as unknown as PluginContext, | ||
| { external } as unknown as NormalizedInputOptions, | ||
| ); | ||
| return warn; | ||
| } | ||
|
|
||
| it('warns when instrumented modules are externalized', () => { | ||
| const warn = runBuildStart(source => source === 'mysql' || source === 'pg'); | ||
|
|
||
| expect(warn).toHaveBeenCalledTimes(1); | ||
| expect(warn).toHaveBeenCalledWith(expect.stringContaining('mysql, pg')); | ||
| expect(warn).toHaveBeenCalledWith(expect.stringContaining('need to be bundled')); | ||
| }); | ||
|
|
||
| it('does not warn when no instrumented modules are externalized', () => { | ||
| const warn = runBuildStart(source => source === 'some-other-package'); | ||
|
|
||
| expect(warn).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('sentryOrchestrionPlugin (esbuild)', () => { | ||
| function runSetup(external: string[] | undefined): OnStartResult[] { | ||
| const onStartCallbacks: Array<() => OnStartResult> = []; | ||
| const build = { | ||
| initialOptions: { external }, | ||
| onStart: (callback: () => OnStartResult) => onStartCallbacks.push(callback), | ||
| } as unknown as PluginBuild; | ||
| void esbuildPlugin().setup(build); | ||
| return onStartCallbacks.map(callback => callback()); | ||
| } | ||
|
|
||
| it('warns when instrumented modules are externalized', () => { | ||
| const results = runSetup(['mysql', 'pg/lib/client']); | ||
|
|
||
| expect(results).toHaveLength(1); | ||
| expect(results[0]?.warnings?.[0]?.text).toContain('mysql, pg'); | ||
| }); | ||
|
|
||
| it('matches wildcard external patterns', () => { | ||
| const results = runSetup(['mysql*']); | ||
|
|
||
| expect(results).toHaveLength(1); | ||
| expect(results[0]?.warnings?.[0]?.text).toContain('mysql'); | ||
| expect(results[0]?.warnings?.[0]?.text).toContain('mysql2'); | ||
| }); | ||
|
|
||
| it('does not warn when no instrumented modules are externalized', () => { | ||
| expect(runSetup(['lodash'])).toHaveLength(0); | ||
| expect(runSetup(undefined)).toHaveLength(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe('sentryOrchestrionWebpackPlugin', () => { | ||
| function runApply(externals: unknown): Error[] { | ||
| const compilation = { warnings: [] as Error[] }; | ||
| const compiler = { | ||
| options: { externals }, | ||
| hooks: { | ||
| thisCompilation: { tap: (_name: string, callback: (compilation: unknown) => void) => callback(compilation) }, | ||
| }, | ||
| webpack: { WebpackError: Error }, | ||
| } as unknown as Compiler; | ||
| sentryOrchestrionWebpackPlugin().apply(compiler); | ||
| return compilation.warnings; | ||
| } | ||
|
|
||
| it('warns for string, RegExp and object externals', () => { | ||
| expect(runApply(['mysql'])[0]?.message).toContain('mysql'); | ||
| expect(runApply('mysql')[0]?.message).toContain('mysql'); | ||
| expect(runApply([/^pg$/])[0]?.message).toContain('pg'); | ||
| expect(runApply({ mysql: 'commonjs mysql' })[0]?.message).toContain('mysql'); | ||
| }); | ||
|
|
||
| it('does not warn for unrelated or function externals', () => { | ||
| expect(runApply(['lodash'])).toHaveLength(0); | ||
| expect(runApply(() => undefined)).toHaveLength(0); | ||
| expect(runApply(undefined)).toHaveLength(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe('sentryOrchestrionPlugin (vite)', () => { | ||
| function runConfigResolved(ssrExternal: string[] | true | undefined): ReturnType<typeof vi.fn> { | ||
| const warn = vi.fn(); | ||
| const plugin = vitePlugin(); | ||
| (plugin.configResolved as (config: unknown) => void)({ | ||
| ssr: { external: ssrExternal }, | ||
| logger: { warn }, | ||
| } as unknown as ResolvedConfig); | ||
| return warn; | ||
| } | ||
|
|
||
| it('warns when instrumented modules are listed in ssr.external', () => { | ||
| const warn = runConfigResolved(['mysql', 'lodash']); | ||
|
|
||
| expect(warn).toHaveBeenCalledTimes(1); | ||
| expect(warn).toHaveBeenCalledWith(expect.stringContaining('mysql')); | ||
| }); | ||
|
|
||
| it('does not warn for ssr.external: true or unrelated entries', () => { | ||
| // `ssr.external: true` does not override the plugin's noExternal entries. | ||
| expect(runConfigResolved(true)).not.toHaveBeenCalled(); | ||
| expect(runConfigResolved(['lodash'])).not.toHaveBeenCalled(); | ||
| expect(runConfigResolved(undefined)).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feat PR lacks integration testsLow Severity This is a Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit b496d7c. Configure here. |
||
|
|
||
| describe('getTracingHooksDirectory', () => { | ||
| it('returns the tracing-hooks package directory with the runtime hook entry points', () => { | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wildcard subpaths skip esbuild warning
Low Severity
When an esbuild
externalentry contains*, matching only regex-tests the bare package name. Patterns likemysql/*therefore never warn, even though the non-wildcard path correctly treats subpath externals such asmysql/lib/...as covering the instrumented package.Reviewed by Cursor Bugbot for commit b496d7c. Configure here.