Skip to content

Commit 297f508

Browse files
authored
ref(core): Do not send telemetry data for self-hosted users (#120)
This PR turns telemetry off for self-hosted users, meaning that the plugin will only send Sentry events to Sentry.io if the URL used in the plugin (i.e. where sourcemaps are uploaded to) is sentry.io. Unfortunately, this isn't entirely straight forward. Sentry-CLI will read a config file (`.sentryclirc`) if it finds one. Either in the directory of a global CLI installation (default behaviour) or if a path to a config file was specified via the `configFile` option. At option conversion time, we therefore don't know exactly yet, if the plugin is used for SaaS or self-hosted. We can only check the `url` option and `SENTRY_URL` env variable to make a first decision. Only at a later time, we can make a call to Sentry-CLI (with the `info`) command which returns us the used Sentry server URL. At this point we can finally definitely make the call to leave telemetry running for SaaS or turn sending off for self-hosted. Note that I had to remove a call to Sentry for options validation which would until now have sent an error to Sentry in case of a validation error. However, given that this is mostly a user-facing config problem, I think we're not missing out on important data.
1 parent 4ef3ccf commit 297f508

File tree

7 files changed

+145
-10
lines changed

7 files changed

+145
-10
lines changed

packages/bundler-plugin-core/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
addSpanToTransaction,
1616
captureMinimalError,
1717
makeSentryClient,
18+
turnOffTelemetryForSelfHostedSentry,
1819
} from "./sentry/telemetry";
1920
import { Span, Transaction } from "@sentry/types";
2021
import { createLogger, Logger } from "./sentry/logger";
@@ -108,8 +109,7 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
108109
handleError(
109110
new Error("Options were not set correctly. See output above for more details."),
110111
logger,
111-
internalOptions.errorHandler,
112-
sentryHub
112+
internalOptions.errorHandler
113113
);
114114
}
115115

@@ -145,6 +145,8 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
145145
* Responsible for starting the plugin execution transaction and the release injection span
146146
*/
147147
async buildStart() {
148+
await turnOffTelemetryForSelfHostedSentry(cli, sentryHub);
149+
148150
if (!internalOptions.release) {
149151
internalOptions.release = await cli.releases.proposeVersion();
150152
}

packages/bundler-plugin-core/src/options-mapping.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ export type InternalIncludeEntry = RequiredInternalIncludeEntry &
5858
ignore: string[];
5959
};
6060

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

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

100-
url: userOptions.url, // env var: `SENTRY_URL`
101106
vcsRemote: userOptions.vcsRemote, // env var: `SENTRY_VSC_REMOTE`
102107

103108
// Optional options
@@ -108,6 +113,14 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions
108113
errorHandler: userOptions.errorHandler,
109114
configFile: userOptions.configFile,
110115
};
116+
117+
// We only want to enable telemetry for SaaS users
118+
// This is not the final check (we need to call Sentry CLI at a later point)
119+
// but we can already at this point make a first decision.
120+
// @see `turnOffTelemetryForSelfHostedSentry` (telemetry.ts) for the second check.
121+
options.telemetry = options.telemetry && options.url === SENTRY_SAAS_URL;
122+
123+
return options;
111124
}
112125

113126
/**

packages/bundler-plugin-core/src/sentry/cli.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import SentryCli, { SentryCliReleases } from "@sentry/cli";
22
import { InternalOptions } from "../options-mapping";
33
import { Logger } from "./logger";
44

5-
type SentryDryRunCLI = { releases: Omit<SentryCliReleases, "listDeploys"> };
5+
type SentryDryRunCLI = {
6+
releases: Omit<SentryCliReleases, "listDeploys">;
7+
execute: (args: string[], live: boolean) => Promise<string>;
8+
};
69
export type SentryCLILike = SentryCli | SentryDryRunCLI;
710

811
/**
@@ -65,5 +68,9 @@ function getDryRunCLI(cli: SentryCli, logger: Logger): SentryDryRunCLI {
6568
return Promise.resolve("");
6669
},
6770
},
71+
execute: (args: string[], live: boolean) => {
72+
logger.info("Executing", args, "live:", live);
73+
return Promise.resolve("Executed");
74+
},
6875
};
6976
}

packages/bundler-plugin-core/src/sentry/telemetry.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {
55
makeMain,
66
makeNodeTransport,
77
NodeClient,
8+
Span,
89
} from "@sentry/node";
9-
import { Span } from "@sentry/tracing";
10-
import { InternalOptions } from "../options-mapping";
10+
import { InternalOptions, SENTRY_SAAS_URL } from "../options-mapping";
1111
import { BuildContext } from "../types";
12+
import { SentryCLILike } from "./cli";
1213

1314
export function makeSentryClient(
1415
dsn: string,
@@ -119,3 +120,28 @@ export function addPluginOptionTags(options: InternalOptions, hub: Hub) {
119120

120121
hub.setTag("node", process.version);
121122
}
123+
124+
/**
125+
* Makes a call to SentryCLI to get the Sentry server URL the CLI uses.
126+
*
127+
* We need to check and decide to use telemetry based on the CLI's respone to this call
128+
* because only at this time we checked a possibly existing .sentryclirc file. This file
129+
* could point to another URL than the default URL.
130+
*/
131+
export async function turnOffTelemetryForSelfHostedSentry(cli: SentryCLILike, hub: Hub) {
132+
const cliInfo = await cli.execute(["info"], false);
133+
134+
const url = cliInfo
135+
?.split(/(\r\n|\n|\r)/)[0]
136+
?.replace(/^Sentry Server: /, "")
137+
?.trim();
138+
139+
if (url !== SENTRY_SAAS_URL) {
140+
const client = hub.getClient();
141+
if (client) {
142+
client.getOptions().enabled = false;
143+
client.getOptions().tracesSampleRate = 0;
144+
client.getOptions().sampleRate = 0;
145+
}
146+
}
147+
}

packages/bundler-plugin-core/test/option-mappings.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe("normalizeUserOptions()", () => {
3434
silent: false,
3535
telemetry: true,
3636
injectReleasesMap: false,
37+
url: "https://sentry.io",
3738
});
3839
});
3940

@@ -76,6 +77,7 @@ describe("normalizeUserOptions()", () => {
7677
silent: false,
7778
telemetry: true,
7879
injectReleasesMap: false,
80+
url: "https://sentry.io",
7981
});
8082
});
8183

@@ -104,6 +106,35 @@ describe("normalizeUserOptions()", () => {
104106
process.env["SENTRY_HEADER"] = _original_SENTRY_HEADER;
105107
process.env["CUSTOM_HEADER"] = _original_CUSTOM_HEADER;
106108
});
109+
110+
test.each(["https://sentry.io", undefined])(
111+
"should enable telemetry if `telemetry` is true and Sentry SaaS URL (%s) is used",
112+
(url) => {
113+
const options = {
114+
include: "",
115+
url,
116+
};
117+
118+
expect(normalizeUserOptions(options).telemetry).toBe(true);
119+
}
120+
);
121+
122+
test.each([
123+
[true, "https://selfhostedSentry.io"],
124+
[false, "https://sentry.io"],
125+
[false, "https://selfhostedSentry.io"],
126+
])(
127+
"should disable telemetry if `telemetry` is %s and Sentry SaaS URL (%s) is used",
128+
(telemetry, url) => {
129+
const options = {
130+
include: "",
131+
telemetry,
132+
url,
133+
};
134+
135+
expect(normalizeUserOptions(options).telemetry).toBe(false);
136+
}
137+
);
107138
});
108139

109140
describe("validateOptions", () => {

packages/bundler-plugin-core/test/sentry/telemetry.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,62 @@
11
import { Hub } from "@sentry/node";
22
import { InternalOptions } from "../../src/options-mapping";
3-
import { addPluginOptionTags, captureMinimalError } from "../../src/sentry/telemetry";
3+
import { SentryCLILike } from "../../src/sentry/cli";
4+
import {
5+
addPluginOptionTags,
6+
captureMinimalError,
7+
turnOffTelemetryForSelfHostedSentry,
8+
} from "../../src/sentry/telemetry";
9+
10+
describe("turnOffTelemetryForSelfHostedSentry", () => {
11+
const mockedCLI = {
12+
execute: jest
13+
.fn()
14+
.mockImplementation(() => "Sentry Server: https://sentry.io \nsomeotherstuff\netc"),
15+
};
16+
17+
const options = {
18+
enabled: true,
19+
tracesSampleRate: 1.0,
20+
sampleRate: 1.0,
21+
};
22+
23+
const mockedClient = {
24+
getOptions: jest.fn().mockImplementation(() => options),
25+
};
26+
const mockedHub = {
27+
getClient: jest.fn().mockImplementation(() => {
28+
return mockedClient;
29+
}),
30+
};
31+
32+
afterEach(() => {
33+
jest.resetAllMocks();
34+
mockedCLI.execute.mockImplementation(
35+
() => "Sentry Server: https://sentry.io \nsomeotherstuff\netc"
36+
);
37+
});
38+
39+
it("Should turn telemetry off if CLI returns a URL other than sentry.io", async () => {
40+
mockedCLI.execute.mockImplementation(
41+
() => "Sentry Server: https://selfhostedSentry.io \nsomeotherstuff\netc"
42+
);
43+
await turnOffTelemetryForSelfHostedSentry(
44+
mockedCLI as unknown as SentryCLILike,
45+
mockedHub as unknown as Hub
46+
);
47+
expect(mockedHub.getClient).toHaveBeenCalledTimes(1);
48+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
49+
expect(mockedHub.getClient().getOptions).toHaveBeenCalledTimes(3);
50+
});
51+
52+
it("Should do nothing if CLI returns sentry.io as a URL", async () => {
53+
await turnOffTelemetryForSelfHostedSentry(
54+
mockedCLI as unknown as SentryCLILike,
55+
mockedHub as unknown as Hub
56+
);
57+
expect(mockedHub.getClient).not.toHaveBeenCalled();
58+
});
59+
});
460

561
describe("captureMinimalError", () => {
662
const mockedHub = {

packages/playground/vite.config.smallNodeApp.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export default defineConfig({
2828
ignore: ["!out/vite-smallNodeApp/index.js.map"],
2929
ignoreFile: ".sentryignore",
3030
setCommits: {
31-
repo: "",
32-
commit: "",
31+
repo: "someRepo",
32+
commit: "someCommit",
3333
ignoreMissing: true,
3434
},
3535
deploy: {

0 commit comments

Comments
 (0)