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
2 changes: 2 additions & 0 deletions packages/unplugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"lint": "eslint ./src ./test"
},
"dependencies": {
"@sentry/node": "^7.11.1",
"@sentry/tracing": "^7.11.1",
"axios": "^0.27.2",
"form-data": "^4.0.0",
"magic-string": "0.26.2",
Expand Down
107 changes: 99 additions & 8 deletions packages/unplugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import MagicString from "magic-string";
import { getReleaseName } from "./getReleaseName";
import { Options } from "./types";
import { makeSentryFacade } from "./sentry/facade";
import "@sentry/tracing";
import { addSpanToTransaction, captureMinimalError, makeSentryClient } from "./sentry/telemetry";
import { Span, Transaction } from "@sentry/types";

const defaultOptions: Omit<Options, "include"> = {
//TODO: add default options here as we port over options from the webpack plugin
Expand All @@ -13,6 +16,7 @@ const defaultOptions: Omit<Options, "include"> = {
finalize: true,
url: "https://sentry.io",
ext: ["js", "map", "jsbundle", "bundle"],
telemetry: true,
};

// We prefix the polyfill id with \0 to tell other plugins not to try to load or transform it.
Expand Down Expand Up @@ -75,6 +79,30 @@ const RELEASE_INJECTOR_ID = "\0sentry-release-injector";
const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext) => {
const options = { ...defaultOptions, ...originalOptions };

//TODO: We can get rid of this variable once we have internal plugin options
const telemetryEnabled = options.telemetry === true;

const { hub: sentryHub } = makeSentryClient(
"https://4c2bae7d9fbc413e8f7385f55c515d51@o1.ingest.sentry.io/6690737",
telemetryEnabled,
options.org
);

if (options.telemetry) {
// eslint-disable-next-line no-console
console.log("[Sentry-plugin]", "Sending error and performance telemetry data to Sentry.");
// eslint-disable-next-line no-console
console.log("[Sentry-Plugin]", "To disable telemetry, set `options.telemetry` to `false`.");
}

sentryHub.setTags({
organization: options.org,
project: options.project,
bundler: unpluginMetaContext.framework,
});

sentryHub.setUser({ id: options.org });

function debugLog(...args: unknown[]) {
if (options?.debugLogging) {
// eslint-disable-next-line no-console
Expand All @@ -88,10 +116,29 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
// file is an entrypoint or a non-entrypoint.
const nonEntrypointSet = new Set<string>();

let transaction: Transaction | undefined;
let releaseInjectionSpan: Span | undefined;

return {
name: "sentry-plugin",
enforce: "pre", // needed for Vite to call resolveId hook

/**
* Responsible for starting the plugin execution transaction and the release injection span
*/
buildStart() {
transaction = sentryHub.startTransaction({
op: "sentry-unplugin",
name: "plugin-execution",
});
releaseInjectionSpan = addSpanToTransaction(
sentryHub,
transaction,
"release-injection",
"release-injection"
);
},

/**
* Responsible for returning the "sentry-release-injector" ID when we encounter it. We return the ID so load is
* called and we can "virtually" load the module. See `load` hook for more info on why it's virtual.
Expand All @@ -104,9 +151,11 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
* @returns `"sentry-release-injector"` when the imported file is called `"sentry-release-injector"`. Otherwise returns `undefined`.
*/
resolveId(id, importer, { isEntry }) {
debugLog(
`Called "resolveId": ${JSON.stringify({ id, importer: importer, options: { isEntry } })}`
);
sentryHub.addBreadcrumb({
category: "resolveId",
message: `isEntry: ${String(isEntry)}`,
level: "info",
});

if (!isEntry) {
nonEntrypointSet.add(id);
Expand All @@ -133,7 +182,10 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
* @returns The global injector code when we load the "sentry-release-injector" module. Otherwise returns `undefined`.
*/
load(id) {
debugLog(`Called "transform": ${JSON.stringify({ id })}`);
sentryHub.addBreadcrumb({
category: "load",
level: "info",
});

if (id === RELEASE_INJECTOR_ID) {
return generateGlobalInjectorCode({ release: getReleaseName(options.release) });
Expand All @@ -151,7 +203,10 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
* want to transform the release injector file.
*/
transformInclude(id) {
debugLog(`Called "transformInclude": ${JSON.stringify({ id })}`);
sentryHub.addBreadcrumb({
category: "transformInclude",
level: "info",
});

if (options.entries) {
// If there's an `entries` option transform (ie. inject the release varible) when the file path matches the option.
Expand Down Expand Up @@ -184,8 +239,11 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
* @param id Always the absolute (fully resolved) path to the module.
* @returns transformed code + source map
*/
transform(code, id) {
debugLog(`Called "transform": ${JSON.stringify({ code, id })}`);
transform(code) {
sentryHub.addBreadcrumb({
category: "transform",
level: "info",
});

// The MagicString library allows us to generate sourcemaps for the changes we make to the user code.
const ms: MagicString = new MagicString(code); // Very stupid author's note: For some absurd reason, when we add a JSDoc to this hook, the TS language server starts complaining about `ms` and adding a type annotation helped so that's why it's here. (┛ಠ_ಠ)┛彡┻━┻
Expand All @@ -206,15 +264,37 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
};
}
},

/**
* Responsible for executing the sentry release creation pipeline (i.e. creating a release on
* Sentry.io, uploading sourcemaps, associating commits and deploys and finalizing the release)
*/
buildEnd() {
releaseInjectionSpan?.finish();
const releasePipelineSpan =
sentryHub &&
transaction &&
addSpanToTransaction(
sentryHub,
transaction,
"release-creation",
"release-creation-pipeline"
);

const release = getReleaseName(options.release);

sentryHub.addBreadcrumb({
category: "buildEnd:start",
level: "info",
});

//TODO:
// 1. validate options to see if we get a valid include property, release name, etc.
// 2. normalize the include property: Users can pass string | string [] | IncludeEntry[].
// That's good for them but a hassle for us. Let's try to normalize this into one data type
// (I vote IncludeEntry[]) and continue with that down the line

const sentryFacade = makeSentryFacade(release, options);
const sentryFacade = makeSentryFacade(release, options, sentryHub);

sentryFacade
.createNewRelease()
Expand All @@ -226,7 +306,18 @@ const unplugin = createUnplugin<Options>((originalOptions, unpluginMetaContext)
.catch((e) => {
//TODO: invoke error handler here
// https://github.com/getsentry/sentry-webpack-plugin/blob/137503f3ac6fe423b16c5c50379859c86e689017/src/index.js#L540-L547
captureMinimalError(e, sentryHub);
transaction?.setStatus("cancelled");
debugLog(e);
})
.finally(() => {
sentryHub.addBreadcrumb({
category: "buildEnd:finish",
level: "info",
});
releasePipelineSpan?.finish();
transaction?.setStatus("ok");
transaction?.finish();
});
},
};
Expand Down
37 changes: 19 additions & 18 deletions packages/unplugin/src/sentry/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,29 @@ import FormData from "form-data";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { version as unpluginVersion } from "../../package.json";
import { captureMinimalError } from "./telemetry";
import { Hub } from "@sentry/node";

const API_PATH = "/api/0";

// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
const USER_AGENT = `sentry-unplugin/${unpluginVersion}`;
const sentryApiAxiosInstance = axios.create();
sentryApiAxiosInstance.interceptors.request.use((config) => {
return {
...config,
headers: {
...config.headers,
"User-Agent": USER_AGENT,
},
};
});
const sentryApiAxiosInstance = axios.create({ headers: { "User-Agent": USER_AGENT } });

export async function createRelease({
org,
project,
release,
authToken,
sentryUrl,
sentryHub,
}: {
release: string;
project: string;
org: string;
authToken: string;
sentryUrl: string;
sentryHub: Hub;
}): Promise<void> {
const requestUrl = `${sentryUrl}${API_PATH}/organizations/${org}/releases/`;

Expand All @@ -49,8 +44,8 @@ export async function createRelease({
headers: { Authorization: `Bearer ${authToken}` },
});
} catch (e) {
// TODO: Maybe do some more sopthisticated error handling here
throw new Error("Something went wrong while creating a release");
captureMinimalError(e, sentryHub);
throw e;
}
}

Expand All @@ -60,12 +55,14 @@ export async function deleteAllReleaseArtifacts({
release,
authToken,
sentryUrl,
sentryHub,
}: {
org: string;
release: string;
sentryUrl: string;
authToken: string;
project: string;
sentryHub: Hub;
}): Promise<void> {
const requestUrl = `${sentryUrl}${API_PATH}/projects/${org}/${project}/files/source-maps/?name=${release}`;

Expand All @@ -76,8 +73,8 @@ export async function deleteAllReleaseArtifacts({
},
});
} catch (e) {
// TODO: Maybe do some more sopthisticated error handling here
throw new Error("Something went wrong while cleaning previous release artifacts");
captureMinimalError(e, sentryHub);
throw e;
}
}

Expand All @@ -87,12 +84,14 @@ export async function updateRelease({
authToken,
sentryUrl,
project,
sentryHub,
}: {
release: string;
org: string;
authToken: string;
sentryUrl: string;
project: string;
sentryHub: Hub;
}): Promise<void> {
const requestUrl = `${sentryUrl}${API_PATH}/projects/${org}/${project}/releases/${release}/`;

Expand All @@ -105,8 +104,8 @@ export async function updateRelease({
headers: { Authorization: `Bearer ${authToken}` },
});
} catch (e) {
// TODO: Maybe do some more sopthisticated error handling here
throw new Error("Something went wrong while creating a release");
captureMinimalError(e, sentryHub);
throw e;
}
}

Expand All @@ -118,6 +117,7 @@ export async function uploadReleaseFile({
sentryUrl,
filename,
fileContent,
sentryHub,
}: {
org: string;
release: string;
Expand All @@ -126,6 +126,7 @@ export async function uploadReleaseFile({
project: string;
filename: string;
fileContent: string;
sentryHub: Hub;
}) {
const requestUrl = `${sentryUrl}${API_PATH}/projects/${org}/${project}/releases/${release}/files/`;

Expand All @@ -141,7 +142,7 @@ export async function uploadReleaseFile({
},
});
} catch (e) {
// TODO: Maybe do some more sopthisticated error handling here
throw new Error(`Something went wrong while uploading file ${filename}`);
captureMinimalError(e, sentryHub);
throw e;
}
}
Loading