Skip to content

Commit 46ba891

Browse files
authored
ref(core): Refactor options validation (#104)
Refactor the validation logic of the plugin options. Since Sentry CLI does most of the validation work out of the box, we only need to take care of a few special cases. Therefore: * Delete the ugly and duplicated org, project, auth, etc. checks in the release pipeline steps as most of them aren't necessary at all in that form * Add a centralized validation function that validates certain combinations of options that Sentry CLI doesn't do (these validations are also present in the webpack plugin, albeit in a little different form) * Add special validation of the `release` value as it simply has to be present to continue with release injection and the release creation pipeline * All validation errors will cause the plugin to throw unless the user specified an error handler. Happy to discuss this point but I think it makes sense to only continue with valid options.
1 parent ce610f9 commit 46ba891

File tree

8 files changed

+236
-148
lines changed

8 files changed

+236
-148
lines changed

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

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
import "@sentry/tracing";
1313
import { addSpanToTransaction, captureMinimalError, makeSentryClient } from "./sentry/telemetry";
1414
import { Span, Transaction } from "@sentry/types";
15-
import { createLogger } from "./sentry/logger";
16-
import { normalizeUserOptions } from "./options-mapping";
15+
import { createLogger, Logger } from "./sentry/logger";
16+
import { InternalOptions, normalizeUserOptions, validateOptions } from "./options-mapping";
1717
import { getSentryCli } from "./sentry/cli";
18+
import { Hub } from "@sentry/node";
1819

1920
// We prefix the polyfill id with \0 to tell other plugins not to try to load or transform it.
2021
// This hack is taken straight from https://rollupjs.org/guide/en/#resolveid.
@@ -78,8 +79,7 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
7879

7980
const { hub: sentryHub } = makeSentryClient(
8081
"https://[email protected]/6690737",
81-
internalOptions.telemetry,
82-
internalOptions.org
82+
internalOptions.telemetry
8383
);
8484

8585
const logger = createLogger({
@@ -89,6 +89,15 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
8989
debug: internalOptions.debug,
9090
});
9191

92+
if (!validateOptions(internalOptions, logger)) {
93+
handleError(
94+
new Error("Options were not set correctly. See output above for more details."),
95+
logger,
96+
internalOptions.errorHandler,
97+
sentryHub
98+
);
99+
}
100+
92101
const cli = getSentryCli(internalOptions, logger);
93102

94103
if (internalOptions.telemetry) {
@@ -125,6 +134,18 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
125134
internalOptions.release = await cli.releases.proposeVersion();
126135
}
127136

137+
// At this point, we either have determined a release or we have to bail
138+
if (!internalOptions.release) {
139+
handleError(
140+
new Error(
141+
"Unable to determine a release name. Make sure to set the `release` option or use an environment that supports auto-detection https://docs.sentry.io/cli/releases/#creating-releases`"
142+
),
143+
logger,
144+
internalOptions.errorHandler,
145+
sentryHub
146+
);
147+
}
148+
128149
transaction = sentryHub.startTransaction({
129150
op: "function.plugin",
130151
name: "Sentry Bundler Plugin execution",
@@ -282,12 +303,6 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
282303
level: "info",
283304
});
284305

285-
//TODO:
286-
// 1. validate options to see if we get a valid include property, release name, etc.
287-
// 2. normalize the include property: Users can pass string | string [] | IncludeEntry[].
288-
// That's good for them but a hassle for us. Let's try to normalize this into one data type
289-
// (I vote IncludeEntry[]) and continue with that down the line
290-
291306
const ctx: BuildContext = { hub: sentryHub, parentSpan: releasePipelineSpan, logger, cli };
292307

293308
createNewRelease(internalOptions, ctx)
@@ -298,16 +313,8 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
298313
.then(() => addDeploy(internalOptions, ctx))
299314
.then(() => transaction?.setStatus("ok"))
300315
.catch((e: Error) => {
301-
captureMinimalError(e, sentryHub);
302316
transaction?.setStatus("cancelled");
303-
304-
logger.error(e.message);
305-
306-
if (internalOptions.errorHandler) {
307-
internalOptions.errorHandler(e);
308-
} else {
309-
throw e;
310-
}
317+
handleError(e, logger, internalOptions.errorHandler, sentryHub);
311318
})
312319
.finally(() => {
313320
sentryHub.addBreadcrumb({
@@ -321,6 +328,25 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
321328
};
322329
});
323330

331+
function handleError(
332+
error: Error,
333+
logger: Logger,
334+
errorHandler: InternalOptions["errorHandler"],
335+
sentryHub?: Hub
336+
) {
337+
logger.error(error.message);
338+
339+
if (sentryHub) {
340+
captureMinimalError(error, sentryHub);
341+
}
342+
343+
if (errorHandler) {
344+
errorHandler(error);
345+
} else {
346+
throw error;
347+
}
348+
}
349+
324350
/**
325351
* Generates code for the "sentry-release-injector" which is responsible for setting the global `SENTRY_RELEASE`
326352
* variable.

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Logger } from "./sentry/logger";
12
import { IncludeEntry as UserIncludeEntry, Options as UserOptions } from "./types";
23
import { arrayify } from "./utils";
34

@@ -175,3 +176,53 @@ function normalizeIncludeEntry(
175176
validate: includeEntry.validate ?? userOptions.validate ?? false,
176177
};
177178
}
179+
180+
/**
181+
* Validates a few combinations of options that are not checked by Sentry CLI.
182+
*
183+
* For all other options, we can rely on Sentry CLI to validate them. In fact,
184+
* we can't validate them in the plugin because Sentry CLI might pick up options from
185+
* its config file.
186+
*
187+
* @param options the internal options
188+
* @param logger the logger
189+
*
190+
* @returns `true` if the options are valid, `false` otherwise
191+
*/
192+
export function validateOptions(options: InternalOptions, logger: Logger): boolean {
193+
if (options.injectReleasesMap && !options.org) {
194+
logger.error(
195+
"The `injectReleasesMap` option was set but it is only supported when the `org` option is also specified.",
196+
"Please set the `org` option (you can also set the SENTRY_ORG environment variable) or disable the `injectReleasesMap` option."
197+
);
198+
return false;
199+
}
200+
201+
const setCommits = options.setCommits;
202+
if (setCommits) {
203+
if (!setCommits.auto && !(setCommits.repo && setCommits.commit)) {
204+
logger.error(
205+
"The `setCommits` option was specified but is missing required properties.",
206+
"Please set either `auto` or both, `repo` and `commit`."
207+
);
208+
return false;
209+
}
210+
if (setCommits.auto && setCommits.repo && setCommits) {
211+
logger.warn(
212+
"The `setCommits` options includes `auto` but also `repo` and `commit`.",
213+
"Ignoring `repo` and `commit`.",
214+
"Please only set either `auto` or both, `repo` and `commit`."
215+
);
216+
}
217+
}
218+
219+
if (options.deploy && !options.deploy.env) {
220+
logger.error(
221+
"The `deploy` option was specified but is missing the required `env` property.",
222+
"Please set the `env` property."
223+
);
224+
return false;
225+
}
226+
227+
return true;
228+
}

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

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,48 +13,21 @@ import { addSpanToTransaction } from "./telemetry";
1313
export async function createNewRelease(options: InternalOptions, ctx: BuildContext): Promise<void> {
1414
const span = addSpanToTransaction(ctx, "function.plugin.create_release");
1515

16-
// TODO: pull these checks out of here and simplify them
17-
if (options.authToken === undefined) {
18-
ctx.logger.warn('Missing "authToken" option. Will not create release.');
19-
return;
20-
} else if (options.org === undefined) {
21-
ctx.logger.warn('Missing "org" option. Will not create release.');
22-
return;
23-
} else if (options.project === undefined) {
24-
ctx.logger.warn('Missing "project" option. Will not create release.');
25-
return;
26-
}
27-
2816
await ctx.cli.releases.new(options.release);
2917

3018
ctx.logger.info("Successfully created release.");
31-
3219
span?.finish();
3320
}
3421

3522
export async function uploadSourceMaps(options: InternalOptions, ctx: BuildContext): Promise<void> {
3623
const span = addSpanToTransaction(ctx, "function.plugin.upload_sourcemaps");
37-
38-
// TODO: pull these checks out of here and simplify them
39-
if (options.authToken === undefined) {
40-
ctx.logger.warn('Missing "authToken" option. Will not create release.');
41-
return Promise.resolve();
42-
} else if (options.org === undefined) {
43-
ctx.logger.warn('Missing "org" option. Will not create release.');
44-
return Promise.resolve();
45-
} else if (options.project === undefined) {
46-
ctx.logger.warn('Missing "project" option. Will not create release.');
47-
return Promise.resolve();
48-
}
49-
5024
ctx.logger.info("Uploading Sourcemaps.");
5125

5226
// Since our internal include entries contain all top-level sourcemaps options,
5327
// we only need to pass the include option here.
5428
await ctx.cli.releases.uploadSourceMaps(options.release, { include: options.include });
5529

5630
ctx.logger.info("Successfully uploaded Sourcemaps.");
57-
5831
span?.finish();
5932
}
6033

@@ -73,20 +46,7 @@ export async function cleanArtifacts(options: InternalOptions, ctx: BuildContext
7346
const span = addSpanToTransaction(ctx, "function.plugin.clean_artifacts");
7447

7548
if (options.cleanArtifacts) {
76-
// TODO: pull these checks out of here and simplify them
77-
if (options.authToken === undefined) {
78-
ctx.logger.warn('Missing "authToken" option. Will not clean existing artifacts.');
79-
return;
80-
} else if (options.org === undefined) {
81-
ctx.logger.warn('Missing "org" option. Will not clean existing artifacts.');
82-
return;
83-
} else if (options.project === undefined) {
84-
ctx.logger.warn('Missing "project" option. Will not clean existing artifacts.');
85-
return;
86-
}
87-
8849
await ctx.cli.releases.execute(["releases", "files", options.release, "delete", "--all"], true);
89-
9050
ctx.logger.info("Successfully cleaned previous artifacts.");
9151
}
9252

@@ -99,22 +59,16 @@ export async function setCommits(options: InternalOptions, ctx: BuildContext): P
9959
if (options.setCommits) {
10060
const { auto, repo, commit, previousCommit, ignoreMissing, ignoreEmpty } = options.setCommits;
10161

102-
if (auto || (repo && commit)) {
103-
await ctx.cli.releases.setCommits(options.release, {
104-
commit,
105-
previousCommit,
106-
repo,
107-
auto,
108-
ignoreMissing,
109-
ignoreEmpty,
110-
});
111-
ctx.logger.info("Successfully set commits.");
112-
} else {
113-
ctx.logger.error(
114-
"Couldn't set commits - neither the `auto` nor the `repo` and `commit` options were specified!",
115-
"Make sure to either set `auto` to `true` or to manually set `repo` and `commit`."
116-
);
117-
}
62+
await ctx.cli.releases.setCommits(options.release, {
63+
commit,
64+
previousCommit,
65+
repo,
66+
auto,
67+
ignoreMissing,
68+
ignoreEmpty,
69+
});
70+
71+
ctx.logger.info("Successfully set commits.");
11872
}
11973

12074
span?.finish();
@@ -126,22 +80,16 @@ export async function addDeploy(options: InternalOptions, ctx: BuildContext): Pr
12680
if (options.deploy) {
12781
const { env, started, finished, time, name, url } = options.deploy;
12882

129-
if (env) {
130-
await ctx.cli.releases.newDeploy(options.release, {
131-
env,
132-
started,
133-
finished,
134-
time,
135-
name,
136-
url,
137-
});
138-
ctx.logger.info("Successfully added deploy.");
139-
} else {
140-
ctx.logger.error(
141-
"Couldn't add deploy - the `env` option was not specified!",
142-
"Make sure to set `deploy.env` (e.g. to 'production')."
143-
);
144-
}
83+
await ctx.cli.releases.newDeploy(options.release, {
84+
env,
85+
started,
86+
finished,
87+
time,
88+
name,
89+
url,
90+
});
91+
92+
ctx.logger.info("Successfully added deploy.");
14593
}
14694

14795
span?.finish();

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ import { BuildContext } from "../types";
1212

1313
export function makeSentryClient(
1414
dsn: string,
15-
telemetryEnabled: boolean,
16-
org?: string
15+
telemetryEnabled: boolean
1716
): { client: NodeClient; hub: Hub } {
1817
const client = new NodeClient({
1918
dsn,
@@ -34,12 +33,6 @@ export function makeSentryClient(
3433

3534
const hub = new Hub(client);
3635

37-
hub.configureScope((scope) => {
38-
if (org) {
39-
scope.setTag("org", org);
40-
}
41-
});
42-
4336
//TODO: This call is problematic because as soon as we set our hub as the current hub
4437
// we might interfere with other plugins that use Sentry. However, for now, we'll
4538
// leave it in because without it, we can't get distributed traces (which are pretty nice)

0 commit comments

Comments
 (0)