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
6 changes: 4 additions & 2 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
addSpanToTransaction,
captureMinimalError,
makeSentryClient,
turnOffTelemetryForSelfHostedSentry,
} from "./sentry/telemetry";
import { Span, Transaction } from "@sentry/types";
import { createLogger, Logger } from "./sentry/logger";
Expand Down Expand Up @@ -108,8 +109,7 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
handleError(
new Error("Options were not set correctly. See output above for more details."),
logger,
internalOptions.errorHandler,
sentryHub
internalOptions.errorHandler
);
}

Expand Down Expand Up @@ -145,6 +145,8 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
* Responsible for starting the plugin execution transaction and the release injection span
*/
async buildStart() {
await turnOffTelemetryForSelfHostedSentry(cli, sentryHub);

if (!internalOptions.release) {
internalOptions.release = await cli.releases.proposeVersion();
}
Expand Down
17 changes: 15 additions & 2 deletions packages/bundler-plugin-core/src/options-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ export type InternalIncludeEntry = RequiredInternalIncludeEntry &
ignore: string[];
};

export const SENTRY_SAAS_URL = "https://sentry.io";

export function normalizeUserOptions(userOptions: UserOptions): InternalOptions {
return {
const options = {
// include is the only strictly required option
// (normalizeInclude needs all userOptions to access top-level include options)
include: normalizeInclude(userOptions),
Expand All @@ -75,6 +77,10 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions
// Sentry CLI to determine a release if none was specified via options
// or env vars. In case we don't find one, we'll bail at that point.
release: userOptions.release ?? process.env["SENTRY_RELEASE"] ?? "",
// We technically don't need the URL for anything release-specific
// but we want to make sure that we're only sending Sentry data
// of SaaS customers. Hence we want to read it anyway.
url: userOptions.url ?? process.env["SENTRY_URL"] ?? SENTRY_SAAS_URL,

// Options with default values
finalize: userOptions.finalize ?? true,
Expand All @@ -97,7 +103,6 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions
customHeader:
userOptions.customHeader ?? process.env["SENTRY_HEADER"] ?? process.env["CUSTOM_HEADER"],

url: userOptions.url, // env var: `SENTRY_URL`
vcsRemote: userOptions.vcsRemote, // env var: `SENTRY_VSC_REMOTE`

// Optional options
Expand All @@ -108,6 +113,14 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions
errorHandler: userOptions.errorHandler,
configFile: userOptions.configFile,
};

// We only want to enable telemetry for SaaS users
// This is not the final check (we need to call Sentry CLI at a later point)
// but we can already at this point make a first decision.
// @see `turnOffTelemetryForSelfHostedSentry` (telemetry.ts) for the second check.
options.telemetry = options.telemetry && options.url === SENTRY_SAAS_URL;

return options;
}

/**
Expand Down
9 changes: 8 additions & 1 deletion packages/bundler-plugin-core/src/sentry/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import SentryCli, { SentryCliReleases } from "@sentry/cli";
import { InternalOptions } from "../options-mapping";
import { Logger } from "./logger";

type SentryDryRunCLI = { releases: Omit<SentryCliReleases, "listDeploys"> };
type SentryDryRunCLI = {
releases: Omit<SentryCliReleases, "listDeploys">;
execute: (args: string[], live: boolean) => Promise<string>;
};
export type SentryCLILike = SentryCli | SentryDryRunCLI;

/**
Expand Down Expand Up @@ -65,5 +68,9 @@ function getDryRunCLI(cli: SentryCli, logger: Logger): SentryDryRunCLI {
return Promise.resolve("");
},
},
execute: (args: string[], live: boolean) => {
logger.info("Executing", args, "live:", live);
return Promise.resolve("Executed");
},
};
}
30 changes: 28 additions & 2 deletions packages/bundler-plugin-core/src/sentry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
makeMain,
makeNodeTransport,
NodeClient,
Span,
} from "@sentry/node";
import { Span } from "@sentry/tracing";
import { InternalOptions } from "../options-mapping";
import { InternalOptions, SENTRY_SAAS_URL } from "../options-mapping";
import { BuildContext } from "../types";
import { SentryCLILike } from "./cli";

export function makeSentryClient(
dsn: string,
Expand Down Expand Up @@ -119,3 +120,28 @@ export function addPluginOptionTags(options: InternalOptions, hub: Hub) {

hub.setTag("node", process.version);
}

/**
* Makes a call to SentryCLI to get the Sentry server URL the CLI uses.
*
* We need to check and decide to use telemetry based on the CLI's respone to this call
* because only at this time we checked a possibly existing .sentryclirc file. This file
* could point to another URL than the default URL.
*/
export async function turnOffTelemetryForSelfHostedSentry(cli: SentryCLILike, hub: Hub) {
const cliInfo = await cli.execute(["info"], false);

const url = cliInfo
?.split(/(\r\n|\n|\r)/)[0]
?.replace(/^Sentry Server: /, "")
?.trim();

if (url !== SENTRY_SAAS_URL) {
const client = hub.getClient();
if (client) {
client.getOptions().enabled = false;
client.getOptions().tracesSampleRate = 0;
client.getOptions().sampleRate = 0;
}
}
}
31 changes: 31 additions & 0 deletions packages/bundler-plugin-core/test/option-mappings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe("normalizeUserOptions()", () => {
silent: false,
telemetry: true,
injectReleasesMap: false,
url: "https://sentry.io",
});
});

Expand Down Expand Up @@ -76,6 +77,7 @@ describe("normalizeUserOptions()", () => {
silent: false,
telemetry: true,
injectReleasesMap: false,
url: "https://sentry.io",
});
});

Expand Down Expand Up @@ -104,6 +106,35 @@ describe("normalizeUserOptions()", () => {
process.env["SENTRY_HEADER"] = _original_SENTRY_HEADER;
process.env["CUSTOM_HEADER"] = _original_CUSTOM_HEADER;
});

test.each(["https://sentry.io", undefined])(
"should enable telemetry if `telemetry` is true and Sentry SaaS URL (%s) is used",
(url) => {
const options = {
include: "",
url,
};

expect(normalizeUserOptions(options).telemetry).toBe(true);
}
);

test.each([
[true, "https://selfhostedSentry.io"],
[false, "https://sentry.io"],
[false, "https://selfhostedSentry.io"],
])(
"should disable telemetry if `telemetry` is %s and Sentry SaaS URL (%s) is used",
(telemetry, url) => {
const options = {
include: "",
telemetry,
url,
};

expect(normalizeUserOptions(options).telemetry).toBe(false);
}
);
});

describe("validateOptions", () => {
Expand Down
58 changes: 57 additions & 1 deletion packages/bundler-plugin-core/test/sentry/telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,62 @@
import { Hub } from "@sentry/node";
import { InternalOptions } from "../../src/options-mapping";
import { addPluginOptionTags, captureMinimalError } from "../../src/sentry/telemetry";
import { SentryCLILike } from "../../src/sentry/cli";
import {
addPluginOptionTags,
captureMinimalError,
turnOffTelemetryForSelfHostedSentry,
} from "../../src/sentry/telemetry";

describe("turnOffTelemetryForSelfHostedSentry", () => {
const mockedCLI = {
execute: jest
.fn()
.mockImplementation(() => "Sentry Server: https://sentry.io \nsomeotherstuff\netc"),
};

const options = {
enabled: true,
tracesSampleRate: 1.0,
sampleRate: 1.0,
};

const mockedClient = {
getOptions: jest.fn().mockImplementation(() => options),
};
const mockedHub = {
getClient: jest.fn().mockImplementation(() => {
return mockedClient;
}),
};

afterEach(() => {
jest.resetAllMocks();
mockedCLI.execute.mockImplementation(
() => "Sentry Server: https://sentry.io \nsomeotherstuff\netc"
);
});

it("Should turn telemetry off if CLI returns a URL other than sentry.io", async () => {
mockedCLI.execute.mockImplementation(
() => "Sentry Server: https://selfhostedSentry.io \nsomeotherstuff\netc"
);
await turnOffTelemetryForSelfHostedSentry(
mockedCLI as unknown as SentryCLILike,
mockedHub as unknown as Hub
);
expect(mockedHub.getClient).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(mockedHub.getClient().getOptions).toHaveBeenCalledTimes(3);
});

it("Should do nothing if CLI returns sentry.io as a URL", async () => {
await turnOffTelemetryForSelfHostedSentry(
mockedCLI as unknown as SentryCLILike,
mockedHub as unknown as Hub
);
expect(mockedHub.getClient).not.toHaveBeenCalled();
});
});

describe("captureMinimalError", () => {
const mockedHub = {
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/vite.config.smallNodeApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export default defineConfig({
ignore: ["!out/vite-smallNodeApp/index.js.map"],
ignoreFile: ".sentryignore",
setCommits: {
repo: "",
commit: "",
repo: "someRepo",
commit: "someCommit",
ignoreMissing: true,
},
deploy: {
Expand Down