Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 57 additions & 29 deletions packages/bundler-plugin-core/src/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = {
/**
Expand Down Expand Up @@ -67,6 +65,15 @@ export type SentryBuildPluginManager = {
*/
createRelease(): Promise<void>;

/**
* 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<void>;

/**
* Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded
*/
Expand All @@ -80,6 +87,20 @@ export type SentryBuildPluginManager = {
createDependencyOnBuildArtifacts: () => () => void;
};

function createCliInstance(options: NormalizedOptions): 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: {
...getTraceData(),
},
});
}

/**
* Creates a build plugin manager that exposes primitives for everything that a Sentry JavaScript SDK or build tooling may do during a build.
*
Expand Down Expand Up @@ -153,6 +174,9 @@ export function createSentryBuildPluginManager(
createDependencyOnBuildArtifacts: () => () => {
/* noop */
},
injectDebugIds: async () => {
/* noop */
},
};
}

Expand Down Expand Up @@ -424,15 +448,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);
Expand Down Expand Up @@ -502,6 +518,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
*/
Expand Down Expand Up @@ -617,23 +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 = 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,
},
});
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
Expand Down
75 changes: 75 additions & 0 deletions packages/bundler-plugin-core/test/build-plugin-manager.test.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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
);
});
});
});
Loading