From 2b2e140bfd1b070814d85d1d96b0fe5b7987c606 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 7 Aug 2025 13:25:18 +0200 Subject: [PATCH 1/2] add injectDebugIds --- .../src/build-plugin-manager.ts | 88 ++++++++++++++----- .../test/build-plugin-manager.test.ts | 75 ++++++++++++++++ 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/packages/bundler-plugin-core/src/build-plugin-manager.ts b/packages/bundler-plugin-core/src/build-plugin-manager.ts index 1cda50c2..a549beb8 100644 --- a/packages/bundler-plugin-core/src/build-plugin-manager.ts +++ b/packages/bundler-plugin-core/src/build-plugin-manager.ts @@ -67,6 +67,15 @@ export type SentryBuildPluginManager = { */ createRelease(): Promise; + /** + * Injects debug IDs into the build artifacts. + * + * This is a separate function from `uploadSourcemaps` because that needs to run before the sourcemaps are uploaded. + * Usually the respective bundler-plugin will take care of this before the sourcemaps are uploaded. + * Only use this if you need to manually inject debug IDs into the build artifacts. + */ + injectDebugIds(buildArtifactPaths: string[]): Promise; + /** * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded */ @@ -80,6 +89,24 @@ export type SentryBuildPluginManager = { createDependencyOnBuildArtifacts: () => () => void; }; +function createCliInstance( + options: NormalizedOptions, + additionalHeaders: Record = {} +): SentryCli { + return new SentryCli(null, { + authToken: options.authToken, + org: options.org, + project: options.project, + silent: options.silent, + url: options.url, + vcsRemote: options.release.vcsRemote, + headers: { + ...options.headers, + ...additionalHeaders, + }, + }); +} + /** * Creates a build plugin manager that exposes primitives for everything that a Sentry JavaScript SDK or build tooling may do during a build. * @@ -153,6 +180,9 @@ export function createSentryBuildPluginManager( createDependencyOnBuildArtifacts: () => () => { /* noop */ }, + injectDebugIds: async () => { + /* noop */ + }, }; } @@ -424,15 +454,7 @@ export function createSentryBuildPluginManager( createDependencyOnBuildArtifacts(); try { - const cliInstance = new SentryCli(null, { - authToken: options.authToken, - org: options.org, - project: options.project, - silent: options.silent, - url: options.url, - vcsRemote: options.release.vcsRemote, - headers: options.headers, - }); + const cliInstance = createCliInstance(options); if (options.release.create) { await cliInstance.releases.new(options.release.name); @@ -502,6 +524,33 @@ export function createSentryBuildPluginManager( } }, + /* + Injects debug IDs into the build artifacts. + + This is a separate function from `uploadSourcemaps` because that needs to run before the sourcemaps are uploaded. + Usually the respective bundler-plugin will take care of this before the sourcemaps are uploaded. + Only use this if you need to manually inject debug IDs into the build artifacts. + */ + async injectDebugIds(buildArtifactPaths: string[]) { + await startSpan( + { name: "inject-debug-ids", scope: sentryScope, forceTransaction: true }, + async () => { + try { + const cliInstance = createCliInstance(options); + await cliInstance.execute( + ["sourcemaps", "inject", ...buildArtifactPaths], + options.debug ?? false + ); + } catch (e) { + sentryScope.captureException('Error in "debugIdInjectionPlugin" writeBundle hook'); + handleRecoverableError(e, false); + } finally { + await safeFlushTelemetry(sentryClient); + } + } + ); + }, + /** * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded */ @@ -618,21 +667,12 @@ export function createSentryBuildPluginManager( setMeasurement("upload_size", uploadSize, "byte", prepBundlesSpan); await startSpan({ name: "upload", scope: sentryScope }, async (uploadSpan) => { - const cliInstance = new SentryCli(null, { - authToken: options.authToken, - org: options.org, - project: options.project, - silent: options.silent, - url: options.url, - vcsRemote: options.release.vcsRemote, - headers: { - "sentry-trace": spanToTraceHeader(uploadSpan), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - baggage: dynamicSamplingContextToSentryBaggageHeader( - getDynamicSamplingContextFromSpan(uploadSpan) - )!, - ...options.headers, - }, + const cliInstance = createCliInstance(options, { + "sentry-trace": spanToTraceHeader(uploadSpan), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + baggage: dynamicSamplingContextToSentryBaggageHeader( + getDynamicSamplingContextFromSpan(uploadSpan) + )!, }); await cliInstance.releases.uploadSourceMaps( diff --git a/packages/bundler-plugin-core/test/build-plugin-manager.test.ts b/packages/bundler-plugin-core/test/build-plugin-manager.test.ts index c0078fa6..47ec645f 100644 --- a/packages/bundler-plugin-core/test/build-plugin-manager.test.ts +++ b/packages/bundler-plugin-core/test/build-plugin-manager.test.ts @@ -1,6 +1,30 @@ import { createSentryBuildPluginManager } from "../src/build-plugin-manager"; +const mockCliExecute = jest.fn(); +jest.mock("@sentry/cli", () => { + return jest.fn().mockImplementation(() => ({ + execute: mockCliExecute, + })); +}); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock("../src/sentry/telemetry", () => ({ + ...jest.requireActual("../src/sentry/telemetry"), + safeFlushTelemetry: jest.fn(), +})); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock("@sentry/core", () => ({ + ...jest.requireActual("@sentry/core"), + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return + startSpan: jest.fn((options, callback) => callback()), +})); + describe("createSentryBuildPluginManager", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("when disabled", () => { it("initializes a no-op build plugin manager", () => { const buildPluginManager = createSentryBuildPluginManager( @@ -48,4 +72,55 @@ describe("createSentryBuildPluginManager", () => { expect(errorSpy).not.toHaveBeenCalled(); }); }); + + describe("injectDebugIds", () => { + it("should call CLI with correct sourcemaps inject command", async () => { + mockCliExecute.mockResolvedValue(undefined); + + const buildPluginManager = createSentryBuildPluginManager( + { + authToken: "test-token", + org: "test-org", + project: "test-project", + }, + { + buildTool: "webpack", + loggerPrefix: "[sentry-webpack-plugin]", + } + ); + + const buildArtifactPaths = ["/path/to/1", "/path/to/2"]; + await buildPluginManager.injectDebugIds(buildArtifactPaths); + + expect(mockCliExecute).toHaveBeenCalledWith( + ["sourcemaps", "inject", "/path/to/1", "/path/to/2"], + false + ); + }); + + it("should pass debug flag when options.debug is true", async () => { + mockCliExecute.mockResolvedValue(undefined); + + const buildPluginManager = createSentryBuildPluginManager( + { + authToken: "test-token", + org: "test-org", + project: "test-project", + debug: true, + }, + { + buildTool: "webpack", + loggerPrefix: "[sentry-webpack-plugin]", + } + ); + + const buildArtifactPaths = ["/path/to/bundle"]; + await buildPluginManager.injectDebugIds(buildArtifactPaths); + + expect(mockCliExecute).toHaveBeenCalledWith( + ["sourcemaps", "inject", "/path/to/bundle"], + true + ); + }); + }); }); From 685911c3f095caba0779916c5cc42a8638ff5d0e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 7 Aug 2025 14:01:43 +0200 Subject: [PATCH 2/2] simplify trace headers? --- .../src/build-plugin-manager.ts | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/bundler-plugin-core/src/build-plugin-manager.ts b/packages/bundler-plugin-core/src/build-plugin-manager.ts index a549beb8..4f5e5193 100644 --- a/packages/bundler-plugin-core/src/build-plugin-manager.ts +++ b/packages/bundler-plugin-core/src/build-plugin-manager.ts @@ -2,10 +2,9 @@ import SentryCli from "@sentry/cli"; import { closeSession, DEFAULT_ENVIRONMENT, - getDynamicSamplingContextFromSpan, + getTraceData, makeSession, setMeasurement, - spanToTraceHeader, startSpan, } from "@sentry/core"; import * as dotenv from "dotenv"; @@ -23,7 +22,6 @@ import { Options, SentrySDKBuildFlags } from "./types"; import { arrayify, getTurborepoEnvPassthroughWarning, stripQueryAndHashFromPath } from "./utils"; import { glob } from "glob"; import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from "./debug-id-upload"; -import { dynamicSamplingContextToSentryBaggageHeader } from "@sentry/utils"; export type SentryBuildPluginManager = { /** @@ -89,10 +87,7 @@ export type SentryBuildPluginManager = { createDependencyOnBuildArtifacts: () => () => void; }; -function createCliInstance( - options: NormalizedOptions, - additionalHeaders: Record = {} -): SentryCli { +function createCliInstance(options: NormalizedOptions): SentryCli { return new SentryCli(null, { authToken: options.authToken, org: options.org, @@ -101,8 +96,7 @@ function createCliInstance( url: options.url, vcsRemote: options.release.vcsRemote, headers: { - ...options.headers, - ...additionalHeaders, + ...getTraceData(), }, }); } @@ -666,14 +660,8 @@ export function createSentryBuildPluginManager( setMeasurement("files", files.length, "none", prepBundlesSpan); setMeasurement("upload_size", uploadSize, "byte", prepBundlesSpan); - await startSpan({ name: "upload", scope: sentryScope }, async (uploadSpan) => { - const cliInstance = createCliInstance(options, { - "sentry-trace": spanToTraceHeader(uploadSpan), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - baggage: dynamicSamplingContextToSentryBaggageHeader( - getDynamicSamplingContextFromSpan(uploadSpan) - )!, - }); + await startSpan({ name: "upload", scope: sentryScope }, async () => { + const cliInstance = createCliInstance(options); await cliInstance.releases.uploadSourceMaps( options.release.name ?? "undefined", // unfortunately this needs a value for now but it will not matter since debug IDs overpower releases anyhow