From e19945efd56d2195bcbf0648d4722a6831ae2386 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 12 Aug 2026 14:25:20 +0200 Subject: [PATCH] feat(core): Add build-time options for bundler plugin parity Co-Authored-By: Claude Opus 5 (1M context) --- packages/bundler-plugins/src/core/types.ts | 28 ++---- .../buildTimeOptionsBase.ts | 77 +++++++++++++++ .../warnOnRemovedBuildOptions.ts | 45 +++++++++ packages/core/src/index.ts | 5 + packages/core/src/metadata.ts | 4 +- packages/core/src/shared-exports.ts | 5 + .../warnOnRemovedBuildOptions.test.ts | 94 +++++++++++++++++++ .../src/vite/sourceMaps.ts | 7 +- .../test/vite/sourceMaps.test.ts | 17 ++++ 9 files changed, 258 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/build-time-plugins/warnOnRemovedBuildOptions.ts create mode 100644 packages/core/test/lib/build-time-plugins/warnOnRemovedBuildOptions.test.ts diff --git a/packages/bundler-plugins/src/core/types.ts b/packages/bundler-plugins/src/core/types.ts index 5f641d3a21a4..7542b3c708b6 100644 --- a/packages/bundler-plugins/src/core/types.ts +++ b/packages/bundler-plugins/src/core/types.ts @@ -1,3 +1,12 @@ +import type { ModuleMetadata, ModuleMetadataCallback, ResolveSourceMapHook } from '@sentry/core'; + +export type { + ModuleMetadata, + ModuleMetadataCallback, + ModuleMetadataCallbackArgs, + ResolveSourceMapHook, +} from '@sentry/core'; + export interface Options { /** * The slug of the Sentry organization associated with the app. @@ -432,25 +441,6 @@ export interface Options { // eslint-disable-next-line @typescript-eslint/no-explicit-any export type RewriteSourcesHook = (source: string, map: any, context?: { mapDir: string }) => string; -export type ResolveSourceMapHook = ( - artifactPath: string, - sourceMappingUrl: string | undefined, -) => string | undefined | Promise; - -export interface ModuleMetadata { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export interface ModuleMetadataCallbackArgs { - org?: string; - project?: string; - projects?: string[]; - release?: string; -} - -export type ModuleMetadataCallback = (args: ModuleMetadataCallbackArgs) => ModuleMetadata; - export type IncludeEntry = { /** * One or more paths to scan for files to upload. diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index eccfc3344f61..744e9c2b0173 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -143,6 +143,71 @@ export interface BuildTimeOptionsBase { * @see https://docs.sentry.io/platforms/javascript/configuration/filtering/#using-thirdpartyerrorfilterintegration */ applicationKey?: string; + + /** + * Metadata that should be associated with the built application. + * + * The metadata is serialized and can be looked up at runtime from within the SDK (for example in + * `beforeSend`, event processors, or the transport), allowing for custom event filtering logic or + * routing of events. Read it at runtime via `moduleMetadataIntegration`. + * + * Metadata can either be passed directly, or as a callback that receives the organization slug, + * the project slug (the first one, when multiple projects are configured), all project slugs, and + * the release name. + */ + moduleMetadata?: ModuleMetadata | ModuleMetadataCallback; +} + +/** + * Arbitrary metadata associated with a built application. + */ +export interface ModuleMetadata { + // oxlint-disable-next-line typescript-eslint/no-explicit-any -- matches the bundler plugin's ModuleMetadata type + [key: string]: any; +} + +/** + * Arguments passed to a {@link ModuleMetadataCallback}. + */ +export interface ModuleMetadataCallbackArgs { + org?: string; + project?: string; + projects?: string[]; + release?: string; +} + +export type ModuleMetadataCallback = (args: ModuleMetadataCallbackArgs) => ModuleMetadata; + +/** + * Hook to customize source map file resolution. + * + * The hook is called with the absolute path of the build artifact and the value of its + * `//# sourceMappingURL=` comment, if present. It should return an absolute path (or a promise + * resolving to one) indicating where to find the artifact's corresponding source map file. If no + * path is returned, or the returned path doesn't exist, the standard resolution process is used. + */ +export type ResolveSourceMapHook = ( + artifactPath: string, + sourceMappingUrl: string | undefined, +) => string | undefined | Promise; + +/** + * Options related to React component name annotations. + * + * Only applicable to React-based SDKs, which is why this is deliberately not part of + * {@link BuildTimeOptionsBase}. + */ +export interface ReactComponentAnnotationOptions { + /** + * Whether the component name annotate plugin should be enabled or not. + */ + enabled?: boolean; + + /** + * A list of strings representing the names of components to ignore. The plugin will not apply + * `data-sentry` annotations on the DOM element for these components. + */ + ignoredComponents?: string[]; } /** @@ -272,6 +337,18 @@ interface SourceMapsOptions { */ // oxlint-disable-next-line typescript-eslint/no-explicit-any -- matches the bundler plugin's RewriteSourcesHook type rewriteSources?: (source: string, map: any, context?: { mapDir: string }) => string; + + /** + * Hook to customize source map file resolution. + * + * Mostly helpful for complex builds with custom source map generation. For example, if source maps + * are written to a separate directory and the `//# sourceMappingURL=` comment is rewritten to + * something other than a relative path, Sentry is unable to locate the source map for a given + * build artifact. This hook lets you implement the resolution process yourself. + * + * Use the `debug` option to print information about source map resolution. + */ + resolveSourceMap?: ResolveSourceMapHook; } type AutoSetCommitsOptions = { diff --git a/packages/core/src/build-time-plugins/warnOnRemovedBuildOptions.ts b/packages/core/src/build-time-plugins/warnOnRemovedBuildOptions.ts new file mode 100644 index 000000000000..a368c8fdd6fc --- /dev/null +++ b/packages/core/src/build-time-plugins/warnOnRemovedBuildOptions.ts @@ -0,0 +1,45 @@ +import { consoleSandbox } from '../utils/debug-logger'; + +/** + * Warns when a build option that was removed in v11 is still present in a user's build config. + * + * TypeScript already rejects these keys, but meta-framework build configs are frequently plain + * JavaScript (`next.config.js`, `next.config.mjs`), where a removed option would otherwise be a + * silent no-op. + * + * @param options The user's build options, if any. + * @param removedKeys The removed option names to check for. + * @param logWarning Called at most once, with the full warning message. Defaults to `console.warn`, + * which suits every SDK whose build step has no logger of its own. Pass this only to route the + * warning through a framework logger (e.g. Astro's). + * + * @internal Only meant for Sentry-internal SDK usage. + * @hidden + */ +// TODO(v12): Remove this helper along with the warnings it powers. +export function warnOnRemovedBuildOptions( + options: object | undefined, + removedKeys: string[], + logWarning: (message: string) => void = message => + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn(message); + }), +): void { + if (!options) { + return; + } + + // `in` rather than an `undefined` check: a user who comments out the nested fields leaves the key + // behind with an `undefined` value, and the option is still gone. + const presentKeys = removedKeys.filter(key => key in options); + + if (!presentKeys.length) { + return; + } + + logWarning( + `[Sentry] Removed in v11 and ignored: ${presentKeys.join(', ')}. ` + + 'Set bundler plugin options directly on the Sentry build options.', + ); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3e40ce629795..fb5104301bac 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,8 @@ export * from './shared-exports'; + +// Build-time only: kept off `shared-exports` so it stays out of the `@sentry/core/browser` and +// `@sentry/core/server` entry points, which no build-time code imports. +// TODO(v12): Remove together with the warning itself. +export { warnOnRemovedBuildOptions } from './build-time-plugins/warnOnRemovedBuildOptions'; export * from './server-exports'; export * from './browser-exports'; diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index bedde5626ee5..9a16d14a836e 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -70,7 +70,7 @@ function ensureMetadataStacksAreParsed(parser: StackParser): void { /** * Retrieve metadata for a specific JavaScript file URL. * - * Metadata is injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option. + * Metadata is injected by the Sentry bundler plugins using the `moduleMetadata` config option. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getMetadataForUrl(parser: StackParser, filename: string): any | undefined { @@ -81,7 +81,7 @@ export function getMetadataForUrl(parser: StackParser, filename: string): any | /** * Adds metadata to stack frames. * - * Metadata is injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option. + * Metadata is injected by the Sentry bundler plugins using the `moduleMetadata` config option. */ export function addMetadataToStackFrames(parser: StackParser, event: Event): void { event.exception?.values?.forEach(exception => { diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index 21bc31a4a8b9..8b2ce4d12d45 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -517,6 +517,11 @@ export type { LegacyCSPReport } from './types/csp'; export type { SerializedLog, SerializedLogContainer } from './types/log'; export type { BuildTimeOptionsBase, + ModuleMetadata, + ModuleMetadataCallback, + ModuleMetadataCallbackArgs, + ReactComponentAnnotationOptions, + ResolveSourceMapHook, UnstableVitePluginOptions, UnstableRollupPluginOptions, UnstableWebpackPluginOptions, diff --git a/packages/core/test/lib/build-time-plugins/warnOnRemovedBuildOptions.test.ts b/packages/core/test/lib/build-time-plugins/warnOnRemovedBuildOptions.test.ts new file mode 100644 index 000000000000..c168be265cf1 --- /dev/null +++ b/packages/core/test/lib/build-time-plugins/warnOnRemovedBuildOptions.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from 'vitest'; +import { warnOnRemovedBuildOptions } from '../../../src/build-time-plugins/warnOnRemovedBuildOptions'; + +describe('warnOnRemovedBuildOptions', () => { + it('falls back to console.warn when no logger is given', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + warnOnRemovedBuildOptions({ unstable_sentryVitePluginOptions: {} }, ['unstable_sentryVitePluginOptions']); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('unstable_sentryVitePluginOptions')); + + consoleWarnSpy.mockRestore(); + }); + + it('stays silent by default when no removed key is present', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + warnOnRemovedBuildOptions({ org: 'my-org' }, ['unstable_sentryVitePluginOptions']); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + + it('does not warn when no removed key is present', () => { + const logWarning = vi.fn(); + + warnOnRemovedBuildOptions({ org: 'my-org' }, ['unstable_sentryVitePluginOptions'], logWarning); + + expect(logWarning).not.toHaveBeenCalled(); + }); + + it('does not warn for undefined options', () => { + const logWarning = vi.fn(); + + warnOnRemovedBuildOptions(undefined, ['unstable_sentryVitePluginOptions'], logWarning); + + expect(logWarning).not.toHaveBeenCalled(); + }); + + it('warns once, naming every removed key that is present', () => { + const logWarning = vi.fn(); + + warnOnRemovedBuildOptions( + { unstable_sentryVitePluginOptions: {}, unstable_sentryRollupPluginOptions: {} }, + ['unstable_sentryVitePluginOptions', 'unstable_sentryRollupPluginOptions'], + logWarning, + ); + + expect(logWarning).toHaveBeenCalledTimes(1); + expect(logWarning).toHaveBeenCalledWith( + expect.stringContaining('unstable_sentryVitePluginOptions, unstable_sentryRollupPluginOptions'), + ); + }); + + it('names only the removed keys that are actually present', () => { + const logWarning = vi.fn(); + + warnOnRemovedBuildOptions( + { unstable_sentryVitePluginOptions: {} }, + ['unstable_sentryVitePluginOptions', 'unstable_sentryRollupPluginOptions'], + logWarning, + ); + + expect(logWarning).toHaveBeenCalledWith(expect.not.stringContaining('unstable_sentryRollupPluginOptions')); + }); + + // A user who comments out the nested fields leaves the key behind with an `undefined` value. + // The option is still gone, so the warning still applies. + it('warns when the key is present but explicitly undefined', () => { + const logWarning = vi.fn(); + + warnOnRemovedBuildOptions( + { unstable_sentryVitePluginOptions: undefined }, + ['unstable_sentryVitePluginOptions'], + logWarning, + ); + + expect(logWarning).toHaveBeenCalledTimes(1); + }); + + it('points users at the first-class replacement', () => { + const logWarning = vi.fn(); + + warnOnRemovedBuildOptions( + { unstable_sentryVitePluginOptions: {} }, + ['unstable_sentryVitePluginOptions'], + logWarning, + ); + + expect(logWarning).toHaveBeenCalledWith(expect.stringContaining('Sentry build options')); + }); +}); diff --git a/packages/tanstackstart-react/src/vite/sourceMaps.ts b/packages/tanstackstart-react/src/vite/sourceMaps.ts index c331617f403d..742f4227758b 100644 --- a/packages/tanstackstart-react/src/vite/sourceMaps.ts +++ b/packages/tanstackstart-react/src/vite/sourceMaps.ts @@ -15,6 +15,7 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl debug, errorHandler, headers, + moduleMetadata, org, project, release, @@ -60,6 +61,7 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl debug: debug ?? false, errorHandler, headers, + moduleMetadata, org: org ?? process.env.SENTRY_ORG, project: project ?? process.env.SENTRY_PROJECT, release, @@ -68,9 +70,8 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl assets: sourcemaps?.assets, disable: sourcemaps?.disable, ignore: sourcemaps?.ignore, - // BuildTimeOptionsBase types can lag behind bundler plugin options in some local setups. - // Keep runtime support while staying resilient to type version skew. - rewriteSources: (sourcemaps as unknown as { rewriteSources?: unknown } | undefined)?.rewriteSources as never, + rewriteSources: sourcemaps?.rewriteSources, + resolveSourceMap: sourcemaps?.resolveSourceMap, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, }, reactComponentAnnotation: { diff --git a/packages/tanstackstart-react/test/vite/sourceMaps.test.ts b/packages/tanstackstart-react/test/vite/sourceMaps.test.ts index e1abdd8865ed..abf8f24301da 100644 --- a/packages/tanstackstart-react/test/vite/sourceMaps.test.ts +++ b/packages/tanstackstart-react/test/vite/sourceMaps.test.ts @@ -98,6 +98,23 @@ describe('makeAddSentryVitePlugin()', () => { ); }); + it('passes moduleMetadata and sourcemaps hooks to sentryVitePlugin', () => { + const rewriteSources = (source: string): string => source; + const resolveSourceMap = (artifactPath: string): string => `${artifactPath}.map`; + + makeAddSentryVitePlugin({ + moduleMetadata: { team: 'sdk' }, + sourcemaps: { rewriteSources, resolveSourceMap }, + }); + + expect(sentryVitePluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ + moduleMetadata: { team: 'sdk' }, + sourcemaps: expect.objectContaining({ rewriteSources, resolveSourceMap }), + }), + ); + }); + it('passes applicationKey to sentryVitePlugin', () => { makeAddSentryVitePlugin({ applicationKey: 'my-app-key',