From 02f2737c192d5d0a66a91b4f4f2c69968e274bda Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 20 May 2025 13:12:34 +0200 Subject: [PATCH] fix(core): Avoid console output and telemetry init when plugins are disabled --- .../src/build-plugin-manager.ts | 91 ++++++++++++++++++- .../src/options-mapping.ts | 84 ++++++++++++++++- .../test/build-plugin-manager.test.ts | 51 +++++++++++ 3 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 packages/bundler-plugin-core/test/build-plugin-manager.test.ts diff --git a/packages/bundler-plugin-core/src/build-plugin-manager.ts b/packages/bundler-plugin-core/src/build-plugin-manager.ts index 1e7f6f81..d8e6a525 100644 --- a/packages/bundler-plugin-core/src/build-plugin-manager.ts +++ b/packages/bundler-plugin-core/src/build-plugin-manager.ts @@ -12,8 +12,8 @@ import * as dotenv from "dotenv"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { normalizeUserOptions, validateOptions } from "./options-mapping"; -import { createLogger } from "./logger"; +import { NormalizedOptions, normalizeUserOptions, validateOptions } from "./options-mapping"; +import { createLogger, Logger } from "./logger"; import { allowedToSendTelemetry, createSentryInstance, @@ -25,7 +25,60 @@ import { glob } from "glob"; import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from "./debug-id-upload"; import { dynamicSamplingContextToSentryBaggageHeader } from "@sentry/utils"; -export type SentryBuildPluginManager = ReturnType; +export type SentryBuildPluginManager = { + /** + * A logger instance that takes the options passed to the build plugin manager into account. (for silencing and log level etc.) + */ + logger: Logger; + + /** + * Options after normalization. Includes things like the inferred release name. + */ + normalizedOptions: NormalizedOptions; + /** + * Magic strings and their replacement values that can be used for bundle size optimizations. This already takes + * into account the options passed to the build plugin manager. + */ + bundleSizeOptimizationReplacementValues: SentrySDKBuildFlags; + /** + * Metadata that should be injected into bundles if possible. Takes into account options passed to the build plugin manager. + */ + // See `generateModuleMetadataInjectorCode` for how this should be used exactly + bundleMetadata: Record; + + /** + * Contains utility functions for emitting telemetry via the build plugin manager. + */ + telemetry: { + /** + * Emits a `Sentry Bundler Plugin execution` signal. + */ + emitBundlerPluginExecutionSignal(): Promise; + }; + + /** + * Will potentially create a release based on the build plugin manager options. + * + * Also + * - finalizes the release + * - sets commits + * - uploads legacy sourcemaps + * - adds deploy information + */ + createRelease(): Promise; + + /** + * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded + */ + uploadSourcemaps(buildArtifactPaths: string[]): Promise; + + /** + * Will delete artifacts based on the passed `sourcemaps.filesToDeleteAfterUpload` option. + */ + deleteArtifacts(): Promise; + + createDependencyOnBuildArtifacts: () => () => void; +}; /** * Creates a build plugin manager that exposes primitives for everything that a Sentry JavaScript SDK or build tooling may do during a build. @@ -44,7 +97,7 @@ export function createSentryBuildPluginManager( */ loggerPrefix: string; } -) { +): SentryBuildPluginManager { const logger = createLogger({ prefix: bundlerPluginMetaContext.loggerPrefix, silent: userOptions.silent ?? false, @@ -73,6 +126,36 @@ export function createSentryBuildPluginManager( const options = normalizeUserOptions(userOptions); + if (options.disable) { + // Early-return a noop build plugin manager instance so that we + // don't continue validating options, setting up Sentry, etc. + // Otherwise we might create side-effects or log messages that + // users don't expect from a disabled plugin. + return { + normalizedOptions: options, + logger, + bundleSizeOptimizationReplacementValues: {}, + telemetry: { + emitBundlerPluginExecutionSignal: async () => { + /* noop */ + }, + }, + bundleMetadata: {}, + createRelease: async () => { + /* noop */ + }, + uploadSourcemaps: async () => { + /* noop */ + }, + deleteArtifacts: async () => { + /* noop */ + }, + createDependencyOnBuildArtifacts: () => () => { + /* noop */ + }, + }; + } + const shouldSendTelemetry = allowedToSendTelemetry(options); const { sentryScope, sentryClient } = createSentryInstance( options, diff --git a/packages/bundler-plugin-core/src/options-mapping.ts b/packages/bundler-plugin-core/src/options-mapping.ts index 55998ce0..1ab3a532 100644 --- a/packages/bundler-plugin-core/src/options-mapping.ts +++ b/packages/bundler-plugin-core/src/options-mapping.ts @@ -1,12 +1,90 @@ import { Logger } from "./logger"; -import { Options as UserOptions, SetCommitsOptions } from "./types"; +import { + Options as UserOptions, + SetCommitsOptions, + RewriteSourcesHook, + ResolveSourceMapHook, + IncludeEntry, + ModuleMetadata, + ModuleMetadataCallback, +} from "./types"; import { determineReleaseName } from "./utils"; -export type NormalizedOptions = ReturnType; +export type NormalizedOptions = { + org: string | undefined; + project: string | undefined; + authToken: string | undefined; + url: string; + headers: Record | undefined; + debug: boolean; + silent: boolean; + errorHandler: ((err: Error) => void) | undefined; + telemetry: boolean; + disable: boolean; + sourcemaps: + | { + disable?: boolean; + assets?: string | string[]; + ignore?: string | string[]; + rewriteSources?: RewriteSourcesHook; + resolveSourceMap?: ResolveSourceMapHook; + filesToDeleteAfterUpload?: string | string[] | Promise; + } + | undefined; + release: { + name: string | undefined; + inject: boolean; + create: boolean; + finalize: boolean; + vcsRemote: string; + setCommits: + | (SetCommitsOptions & { + shouldNotThrowOnFailure?: boolean; + }) + | false + | undefined; + dist?: string; + deploy?: { + env: string; + started?: number | string; + finished?: number | string; + time?: number; + name?: string; + url?: string; + }; + uploadLegacySourcemaps?: string | IncludeEntry | Array; + }; + bundleSizeOptimizations: + | { + excludeDebugStatements?: boolean; + excludeTracing?: boolean; + excludeReplayCanvas?: boolean; + excludeReplayShadowDom?: boolean; + excludeReplayIframe?: boolean; + excludeReplayWorker?: boolean; + } + | undefined; + reactComponentAnnotation: + | { + enabled?: boolean; + ignoredComponents?: string[]; + } + | undefined; + _metaOptions: { + telemetry: { + metaFramework: string | undefined; + }; + }; + applicationKey: string | undefined; + moduleMetadata: ModuleMetadata | ModuleMetadataCallback | undefined; + _experiments: { + injectBuildInformation?: boolean; + } & Record; +}; export const SENTRY_SAAS_URL = "https://sentry.io"; -export function normalizeUserOptions(userOptions: UserOptions) { +export function normalizeUserOptions(userOptions: UserOptions): NormalizedOptions { const options = { org: userOptions.org ?? process.env["SENTRY_ORG"], project: userOptions.project ?? process.env["SENTRY_PROJECT"], diff --git a/packages/bundler-plugin-core/test/build-plugin-manager.test.ts b/packages/bundler-plugin-core/test/build-plugin-manager.test.ts new file mode 100644 index 00000000..c0078fa6 --- /dev/null +++ b/packages/bundler-plugin-core/test/build-plugin-manager.test.ts @@ -0,0 +1,51 @@ +import { createSentryBuildPluginManager } from "../src/build-plugin-manager"; + +describe("createSentryBuildPluginManager", () => { + describe("when disabled", () => { + it("initializes a no-op build plugin manager", () => { + const buildPluginManager = createSentryBuildPluginManager( + { + disable: true, + }, + { + buildTool: "webpack", + loggerPrefix: "[sentry-webpack-plugin]", + } + ); + + expect(buildPluginManager).toBeDefined(); + expect(buildPluginManager.logger).toBeDefined(); + expect(buildPluginManager.normalizedOptions.disable).toBe(true); + }); + + it("does not log anything to the console", () => { + const logSpy = jest.spyOn(console, "log"); + const infoSpy = jest.spyOn(console, "info"); + const debugSpy = jest.spyOn(console, "debug"); + const warnSpy = jest.spyOn(console, "warn"); + const errorSpy = jest.spyOn(console, "error"); + + createSentryBuildPluginManager( + { + disable: true, + release: { + deploy: { + // An empty string triggers a validation error (but satisfies the type checker) + env: "", + }, + }, + }, + { + buildTool: "webpack", + loggerPrefix: "[sentry-webpack-plugin]", + } + ); + + expect(logSpy).not.toHaveBeenCalled(); + expect(infoSpy).not.toHaveBeenCalled(); + expect(debugSpy).not.toHaveBeenCalled(); + expect(warnSpy).not.toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); + }); + }); +});