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
13 changes: 12 additions & 1 deletion packages/bundler-plugin-core/src/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ import {
import { glob } from "glob";
import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from "./debug-id-upload";

// Module-level guard to prevent duplicate deploy records when multiple bundler plugin
// instances run in the same process (e.g. Next.js creates separate webpack compilers
// for client, server, and edge). Keyed by release name.
const _deployedReleases = new Set<string>();

/** @internal Exported for testing only. */
export function _resetDeployedReleasesForTesting(): void {
_deployedReleases.clear();
}

export type SentryBuildPluginManager = {
/**
* A logger instance that takes the options passed to the build plugin manager into account. (for silencing and log level etc.)
Expand Down Expand Up @@ -534,8 +544,9 @@ export function createSentryBuildPluginManager(
await cliInstance.releases.finalize(options.release.name);
}

if (options.release.deploy) {
if (options.release.deploy && !_deployedReleases.has(options.release.name)) {
await cliInstance.releases.newDeploy(options.release.name, options.release.deploy);
_deployedReleases.add(options.release.name);
}
} catch (e) {
sentryScope.captureException('Error in "releaseManagementPlugin" writeBundle hook');
Expand Down
140 changes: 138 additions & 2 deletions packages/bundler-plugin-core/test/build-plugin-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { createSentryBuildPluginManager } from "../src/build-plugin-manager";
import {
createSentryBuildPluginManager,
_resetDeployedReleasesForTesting,
} from "../src/build-plugin-manager";
import fs from "fs";
import { glob } from "glob";
import { prepareBundleForDebugIdUpload } from "../src/debug-id-upload";

const mockCliExecute = jest.fn();
const mockCliUploadSourceMaps = jest.fn();
const mockCliNewDeploy = jest.fn();

jest.mock("@sentry/cli", () => {
return jest.fn().mockImplementation(() => ({
Expand All @@ -14,7 +18,7 @@ jest.mock("@sentry/cli", () => {
new: jest.fn(),
finalize: jest.fn(),
setCommits: jest.fn(),
newDeploy: jest.fn(),
newDeploy: mockCliNewDeploy,
},
}));
});
Expand Down Expand Up @@ -633,4 +637,136 @@ describe("createSentryBuildPluginManager", () => {
});
});
});

describe("createRelease deploy deduplication", () => {
beforeEach(() => {
jest.clearAllMocks();
_resetDeployedReleasesForTesting();
});

it("should create a deploy record on the first call", async () => {
const manager = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: {
name: "test-release",
deploy: { env: "production" },
},
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await manager.createRelease();

expect(mockCliNewDeploy).toHaveBeenCalledTimes(1);
expect(mockCliNewDeploy).toHaveBeenCalledWith("test-release", { env: "production" });
});

it("should not create duplicate deploy records when createRelease is called multiple times on the same instance", async () => {
const manager = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: {
name: "test-release",
deploy: { env: "production" },
},
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await manager.createRelease();
await manager.createRelease();
await manager.createRelease();

expect(mockCliNewDeploy).toHaveBeenCalledTimes(1);
});

it("should not create duplicate deploy records across separate plugin instances with the same release name", async () => {
const managerA = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: {
name: "test-release",
deploy: { env: "production" },
},
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

const managerB = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: {
name: "test-release",
deploy: { env: "production" },
},
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await managerA.createRelease();
await managerB.createRelease();

expect(mockCliNewDeploy).toHaveBeenCalledTimes(1);
});

it("should allow deploys for different release names", async () => {
const managerA = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: {
name: "release-1",
deploy: { env: "production" },
},
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

const managerB = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: {
name: "release-2",
deploy: { env: "production" },
},
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await managerA.createRelease();
await managerB.createRelease();

expect(mockCliNewDeploy).toHaveBeenCalledTimes(2);
expect(mockCliNewDeploy).toHaveBeenCalledWith("release-1", { env: "production" });
expect(mockCliNewDeploy).toHaveBeenCalledWith("release-2", { env: "production" });
});

it("should not create a deploy when deploy option is not set", async () => {
const manager = createSentryBuildPluginManager(
{
authToken: "test-token",
org: "test-org",
project: "test-project",
release: { name: "test-release" },
},
{ buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" }
);

await manager.createRelease();

expect(mockCliNewDeploy).not.toHaveBeenCalled();
});
});
});
Loading