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
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { glob } from "glob";
import os from "os";
import path from "path";
import * as util from "util";
import { UnpluginOptions } from "unplugin";
import { Logger } from "../sentry/logger";
import { Logger } from "./sentry/logger";
import { promisify } from "util";
import { Hub, NodeClient } from "@sentry/node";
import SentryCli from "@sentry/cli";
Expand All @@ -15,7 +14,7 @@ interface RewriteSourcesHook {

interface DebugIdUploadPluginOptions {
logger: Logger;
assets: string | string[];
assets?: string | string[];
ignore?: string | string[];
releaseName?: string;
dist?: string;
Expand All @@ -35,7 +34,7 @@ interface DebugIdUploadPluginOptions {
};
}

export function debugIdUploadPlugin({
export function createDebugIdUploadFunction({
assets,
ignore,
logger,
Expand All @@ -47,34 +46,51 @@ export function debugIdUploadPlugin({
sentryCliOptions,
rewriteSourcesHook,
deleteFilesAfterUpload,
}: DebugIdUploadPluginOptions): UnpluginOptions {
return {
name: "sentry-debug-id-upload-plugin",
async writeBundle() {
let folderToCleanUp: string | undefined;
}: DebugIdUploadPluginOptions) {
return async (buildArtifactPaths: string[]) => {
let folderToCleanUp: string | undefined;

const cliInstance = new SentryCli(null, sentryCliOptions);
const cliInstance = new SentryCli(null, sentryCliOptions);

try {
const tmpUploadFolder = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "sentry-bundler-plugin-upload-")
);
try {
const tmpUploadFolder = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "sentry-bundler-plugin-upload-")
);

folderToCleanUp = tmpUploadFolder;
folderToCleanUp = tmpUploadFolder;

const debugIdChunkFilePaths = (
await glob(assets, {
absolute: true,
nodir: true,
ignore: ignore,
})
).filter(
(debugIdChunkFilePath) =>
debugIdChunkFilePath.endsWith(".js") ||
debugIdChunkFilePath.endsWith(".mjs") ||
debugIdChunkFilePath.endsWith(".cjs")
let globAssets;
if (assets) {
globAssets = assets;
} else {
logger.debug(
"No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts."
);
globAssets = buildArtifactPaths;
}

const debugIdChunkFilePaths = (
await glob(globAssets, {
absolute: true,
nodir: true,
ignore: ignore,
})
).filter(
(debugIdChunkFilePath) =>
debugIdChunkFilePath.endsWith(".js") ||
debugIdChunkFilePath.endsWith(".mjs") ||
debugIdChunkFilePath.endsWith(".cjs")
);

if (Array.isArray(assets) && assets.length === 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is assets always normalized to an array? otherwise, what about assets: ""?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought process here was to just skip uploading entirely when users provide an empty array (as a means of disabling I guess). An empty string is more or less user error which we don't care to catch here. I think this will be implicitly caught by the glob pattern not finding anything and the message below then saying "check your assets option".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point. At the time of writing I thought for some reason an empty array and an empty string the same semantic implications here but you're right, an empty string looks more like an error than an empty array.

logger.debug(
"Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID."
);
} else if (debugIdChunkFilePaths.length === 0) {
logger.warn(
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
);
} else {
await Promise.all(
debugIdChunkFilePaths.map(async (chunkFilePath, chunkIndex): Promise<void> => {
await prepareBundleForDebugIdUpload(
Expand All @@ -100,33 +116,33 @@ export function debugIdUploadPlugin({
useArtifactBundle: true,
}
);
}

if (deleteFilesAfterUpload) {
const filePathsToDelete = await glob(deleteFilesAfterUpload, {
absolute: true,
nodir: true,
});
if (deleteFilesAfterUpload) {
const filePathsToDelete = await glob(deleteFilesAfterUpload, {
absolute: true,
nodir: true,
});

filePathsToDelete.forEach((filePathToDelete) => {
logger.debug(`Deleting asset after upload: ${filePathToDelete}`);
});
filePathsToDelete.forEach((filePathToDelete) => {
logger.debug(`Deleting asset after upload: ${filePathToDelete}`);
});

await Promise.all(
filePathsToDelete.map((filePathToDelete) =>
fs.promises.rm(filePathToDelete, { force: true })
)
);
}
} catch (e) {
sentryHub.captureException('Error in "debugIdUploadPlugin" writeBundle hook');
await sentryClient.flush();
handleRecoverableError(e);
} finally {
if (folderToCleanUp) {
void fs.promises.rm(folderToCleanUp, { recursive: true, force: true });
}
await Promise.all(
filePathsToDelete.map((filePathToDelete) =>
fs.promises.rm(filePathToDelete, { force: true })
)
);
}
} catch (e) {
sentryHub.captureException('Error in "debugIdUploadPlugin" writeBundle hook');
await sentryClient.flush();
handleRecoverableError(e);
} finally {
if (folderToCleanUp) {
void fs.promises.rm(folderToCleanUp, { recursive: true, force: true });
}
},
}
};
}

Expand Down
79 changes: 50 additions & 29 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import SentryCli from "@sentry/cli";
import fs from "fs";
import * as fs from "fs";
import * as path from "path";
import MagicString from "magic-string";
import { createUnplugin, UnpluginOptions } from "unplugin";
import { normalizeUserOptions, validateOptions } from "./options-mapping";
import { debugIdUploadPlugin } from "./plugins/debug-id-upload";
import { createDebugIdUploadFunction } from "./debug-id-upload";
import { releaseManagementPlugin } from "./plugins/release-management";
import { telemetryPlugin } from "./plugins/telemetry";
import { createLogger } from "./sentry/logger";
Expand All @@ -21,6 +22,7 @@ import {
interface SentryUnpluginFactoryOptions {
releaseInjectionPlugin: (injectionCode: string) => UnpluginOptions;
debugIdInjectionPlugin: () => UnpluginOptions;
debugIdUploadPlugin: (upload: (buildArtifacts: string[]) => Promise<void>) => UnpluginOptions;
}

/**
Expand Down Expand Up @@ -53,6 +55,7 @@ interface SentryUnpluginFactoryOptions {
export function sentryUnpluginFactory({
releaseInjectionPlugin,
debugIdInjectionPlugin,
debugIdUploadPlugin,
}: SentryUnpluginFactoryOptions) {
return createUnplugin<Options, true>((userOptions, unpluginMetaContext) => {
const options = normalizeUserOptions(userOptions);
Expand Down Expand Up @@ -180,35 +183,31 @@ export function sentryUnpluginFactory({
);
}

if (options.sourcemaps) {
if (!options.authToken) {
logger.warn(
"No auth token provided. Will not upload source maps. Please set the `authToken` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/"
);
} else if (!options.org) {
logger.warn(
"No org provided. Will not upload source maps. Please set the `org` option to your Sentry organization slug."
);
} else if (!options.project) {
logger.warn(
"No project provided. Will not upload source maps. Please set the `project` option to your Sentry project slug."
);
} else if (!options.sourcemaps.assets) {
logger.warn(
"No assets defined. Will not upload source maps. Please provide set the `assets` option to your build-output folder."
);
} else {
plugins.push(debugIdInjectionPlugin());
plugins.push(
debugIdUploadPlugin({
assets: options.sourcemaps.assets,
ignore: options.sourcemaps.ignore,
deleteFilesAfterUpload: options.sourcemaps.deleteFilesAfterUpload,
if (!options.authToken) {
logger.warn(
"No auth token provided. Will not upload source maps. Please set the `authToken` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/"
);
} else if (!options.org) {
logger.warn(
"No org provided. Will not upload source maps. Please set the `org` option to your Sentry organization slug."
);
} else if (!options.project) {
logger.warn(
"No project provided. Will not upload source maps. Please set the `project` option to your Sentry project slug."
);
} else {
plugins.push(debugIdInjectionPlugin());
plugins.push(
debugIdUploadPlugin(
createDebugIdUploadFunction({
assets: options.sourcemaps?.assets,
ignore: options.sourcemaps?.ignore,
deleteFilesAfterUpload: options.sourcemaps?.deleteFilesAfterUpload,
dist: options.release.dist,
releaseName: options.release.name,
logger: logger,
handleRecoverableError: handleRecoverableError,
rewriteSourcesHook: options.sourcemaps.rewriteSources,
rewriteSourcesHook: options.sourcemaps?.rewriteSources,
sentryHub,
sentryClient,
sentryCliOptions: {
Expand All @@ -221,8 +220,8 @@ export function sentryUnpluginFactory({
headers: options.headers,
},
})
);
}
)
);
}

return plugins;
Expand Down Expand Up @@ -341,6 +340,28 @@ export function createRollupDebugIdInjectionHooks() {
};
}

export function createRollupDebugIdUploadHooks(
upload: (buildArtifacts: string[]) => Promise<void>
) {
return {
async writeBundle(
outputOptions: { dir?: string; file?: string },
bundle: { [fileName: string]: unknown }
) {
if (outputOptions.dir) {
const outputDir = outputOptions.dir;
const buildArtifacts = Object.keys(bundle).map((asset) => path.join(outputDir, asset));
await upload(buildArtifacts);
} else if (outputOptions.file) {
await upload([outputOptions.file]);
} else {
const buildArtifacts = Object.keys(bundle).map((asset) => path.join(path.resolve(), asset));
await upload(buildArtifacts);
}
},
};
}

export function getDebugIdSnippet(debugId: string): string {
return `;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();`;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/bundler-plugin-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@ export interface Options {
/**
* A glob or an array of globs that specifies the build artifacts that should be uploaded to Sentry.
*
* If this option is not specified, the plugin will try to upload all JavaScript files and source map files that are created during build.
*
* The globbing patterns follow the implementation of the `glob` package. (https://www.npmjs.com/package/glob)
*
* Use the `debug` option to print information about which files end up being uploaded.
*/
assets: string | string[];
assets?: string | string[];

/**
* A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry.
Expand Down
2 changes: 1 addition & 1 deletion packages/dev-utils/src/generate-documentation-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ errorHandler: (err) => {
name: "assets",
type: "string | string[]",
fullDescription:
"A glob or an array of globs that specifies the build artifacts that should be uploaded to Sentry.\n\nThe globbing patterns follow the implementation of the `glob` package. (https://www.npmjs.com/package/glob)\n\nUse the `debug` option to print information about which files end up being uploaded.",
"A glob or an array of globs that specifies the build artifacts that should be uploaded to Sentry.\n\nIf this option is not specified, the plugin will try to upload all JavaScript files and source map files that are created during build.\n\nThe globbing patterns follow the implementation of the `glob` package. (https://www.npmjs.com/package/glob)\n\nUse the `debug` option to print information about which files end up being uploaded.",
},
{
name: "ignore",
Expand Down
21 changes: 0 additions & 21 deletions packages/esbuild-plugin/README_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,6 @@ require("esbuild").build({
// Auth tokens can be obtained from https://sentry.io/settings/account/api/auth-tokens/
// and need `project:releases` and `org:read` scopes
authToken: process.env.SENTRY_AUTH_TOKEN,

sourcemaps: {
// Specify the directory containing build artifacts
assets: "./**",
// Don't upload the source maps of dependencies
ignore: ["./node_modules/**"],
},

// Helps troubleshooting - set to false to make plugin less noisy
debug: true,

// Use the following option if you're on an SDK version lower than 7.47.0:
// release: {
// uploadLegacySourcemaps: {
// include: ".",
// ignore: ["node_modules"],
// },
// },

// Optionally uncomment the line below to override automatic release name detection
// release: process.env.RELEASE,
}),
],
});
Expand Down
18 changes: 18 additions & 0 deletions packages/esbuild-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,27 @@ function esbuildDebugIdInjectionPlugin(): UnpluginOptions {
};
}

function esbuildDebugIdUploadPlugin(
upload: (buildArtifacts: string[]) => Promise<void>
): UnpluginOptions {
return {
name: "sentry-esbuild-debug-id-upload-plugin",
esbuild: {
setup({ initialOptions, onEnd }) {
initialOptions.metafile = true;
onEnd(async (result) => {
const buildArtifacts = result.metafile ? Object.keys(result.metafile.outputs) : [];
await upload(buildArtifacts);
});
},
},
};
}

const sentryUnplugin = sentryUnpluginFactory({
releaseInjectionPlugin: esbuildReleaseInjectionPlugin,
debugIdInjectionPlugin: esbuildDebugIdInjectionPlugin,
debugIdUploadPlugin: esbuildDebugIdUploadPlugin,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
5 changes: 1 addition & 4 deletions packages/playground/build-esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ build({
outdir: "./out/esbuild",
plugins: [
sentryEsbuildPlugin({
sourcemaps: {
assets: "./out/esbuild/**",
deleteFilesAfterUpload: "./out/esbuild/**/*.map",
},
debug: true,
}),
],
minify: true,
Expand Down
5 changes: 1 addition & 4 deletions packages/playground/build-webpack4.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ webpack4(
},
plugins: [
sentryWebpackPlugin({
sourcemaps: {
assets: "./out/webpack4/**",
deleteFilesAfterUpload: "./out/webpack4/**/*.map",
},
debug: true,
}),
],
devtool: "source-map",
Expand Down
Loading