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
3 changes: 2 additions & 1 deletion packages/bundler-plugin-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,18 @@
"fix": "eslint ./src ./test --format stylish --fix"
},
"dependencies": {
"@babel/core": "7.18.5",
"@sentry/cli": "^2.22.3",
"@sentry/node": "^7.60.0",
"@sentry/utils": "^7.60.0",
"@sentry/component-annotate-plugin": "2.10.3",
"dotenv": "^16.3.1",
"find-up": "5.0.0",
"glob": "9.3.2",
"magic-string": "0.27.0",
"unplugin": "1.0.1"
},
"devDependencies": {
"@babel/core": "7.18.5",
"@babel/preset-env": "7.18.2",
"@babel/preset-typescript": "7.17.12",
"@rollup/plugin-babel": "5.3.1",
Expand Down
77 changes: 75 additions & 2 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import SentryCli from "@sentry/cli";
import { transformAsync } from "@babel/core";
import { componentNameAnnotatePlugin } from "@sentry/component-annotate-plugin";
import * as fs from "fs";
import * as path from "path";
import MagicString from "magic-string";
import { createUnplugin, UnpluginOptions } from "unplugin";
import { createUnplugin, TransformResult, UnpluginOptions } from "unplugin";
import { normalizeUserOptions, validateOptions } from "./options-mapping";
import { createDebugIdUploadFunction } from "./debug-id-upload";
import { releaseManagementPlugin } from "./plugins/release-management";
Expand All @@ -22,9 +24,12 @@ import {
} from "./utils";
import * as dotenv from "dotenv";
import { glob } from "glob";
import pkg from "@sentry/utils";
const { logger } = pkg;

interface SentryUnpluginFactoryOptions {
releaseInjectionPlugin: (injectionCode: string) => UnpluginOptions;
componentNameAnnotatePlugin?: () => UnpluginOptions;
moduleMetadataInjectionPlugin?: (injectionCode: string) => UnpluginOptions;
debugIdInjectionPlugin: () => UnpluginOptions;
debugIdUploadPlugin: (upload: (buildArtifacts: string[]) => Promise<void>) => UnpluginOptions;
Expand Down Expand Up @@ -60,6 +65,7 @@ interface SentryUnpluginFactoryOptions {
*/
export function sentryUnpluginFactory({
releaseInjectionPlugin,
componentNameAnnotatePlugin,
moduleMetadataInjectionPlugin,
debugIdInjectionPlugin,
debugIdUploadPlugin,
Expand Down Expand Up @@ -317,6 +323,20 @@ export function sentryUnpluginFactory({
);
}

if (options.reactComponentAnnotation) {
if (!options.reactComponentAnnotation.enabled) {
logger.info(
"The component name annotate plugin is currently disabled. Skipping component name annotations."
);
} else if (options.reactComponentAnnotation.enabled && !componentNameAnnotatePlugin) {
logger.warn(
"The component name annotate plugin is currently not supported by '@sentry/esbuild-plugin'"
);
} else {
componentNameAnnotatePlugin && plugins.push(componentNameAnnotatePlugin());
}
}

return plugins;
});
}
Expand Down Expand Up @@ -346,7 +366,6 @@ export function sentryCliBinaryExists(): boolean {

export function createRollupReleaseInjectionHooks(injectionCode: string) {
const virtualReleaseInjectionFileId = "\0sentry-release-injection-file";

return {
resolveId(id: string) {
if (id === virtualReleaseInjectionFileId) {
Expand Down Expand Up @@ -510,6 +529,60 @@ export function createRollupDebugIdUploadHooks(
};
}

export function createComponentNameAnnotateHooks() {
type ParserPlugins = NonNullable<
NonNullable<Parameters<typeof transformAsync>[1]>["parserOpts"]
>["plugins"];

return {
async transform(this: void, code: string, id: string): Promise<TransformResult> {
// id may contain query and hash which will trip up our file extension logic below
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);

if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
return null;
}

// We will only apply this plugin on jsx and tsx files
if (![".jsx", ".tsx"].some((ending) => idWithoutQueryAndHash.endsWith(ending))) {
return null;
}

const parserPlugins: ParserPlugins = [];
if (idWithoutQueryAndHash.endsWith(".jsx")) {
parserPlugins.push("jsx");
} else if (idWithoutQueryAndHash.endsWith(".tsx")) {
parserPlugins.push("jsx", "typescript");
}

try {
const result = await transformAsync(code, {
plugins: [[componentNameAnnotatePlugin]],
filename: id,
parserOpts: {
sourceType: "module",
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
decoratorsBeforeExport: true,
},
sourceMaps: true,
});

return {
code: result?.code ?? code,
map: result?.map,
};
} catch (e) {
logger.error(`Failed to apply react annotate plugin`, e);
}

return { code };
},
};
}

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
1 change: 1 addition & 0 deletions packages/bundler-plugin-core/src/options-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function normalizeUserOptions(userOptions: UserOptions) {
cleanArtifacts: userOptions.release?.cleanArtifacts ?? false,
},
bundleSizeOptimizations: userOptions.bundleSizeOptimizations,
reactComponentAnnotation: userOptions.reactComponentAnnotation,
_experiments: userOptions._experiments ?? {},
};

Expand Down
14 changes: 14 additions & 0 deletions packages/bundler-plugin-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,20 @@ export interface Options {
excludeReplayWorker?: boolean;
};

/**
* Options related to react component name annotations.
* Disabled by default, unless a value is set for this option.
* When enabled, your app's DOM will automatically be annotated during build-time with their respective component names.
* This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring.
* Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components
*/
reactComponentAnnotation?: {
/**
* Whether the component name annotate plugin should be enabled or not.
*/
enabled?: boolean;
};

/**
* Options that are considered experimental and subject to change.
*
Expand Down
3 changes: 1 addition & 2 deletions packages/component-annotate-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@
"clean:build": "rimraf ./dist *.tgz",
"clean:deps": "rimraf node_modules",
"test": "jest",
"lint": "eslint ./src ./test",
"prepack": "ts-node ./src/prepack.ts"
"lint": "eslint ./src ./test"
},
"dependencies": {},
"devDependencies": {
Expand Down
45 changes: 38 additions & 7 deletions packages/dev-utils/src/generate-documentation-table.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
type Bundler = "webpack" | "vite" | "rollup" | "esbuild";

type OptionDocumentation = {
name: string;
fullDescription: string;
type?: string;
children?: OptionDocumentation[];
supportedBundlers?: Bundler[];
};

const options: OptionDocumentation[] = [
Expand Down Expand Up @@ -332,6 +335,24 @@ type IncludeEntry = {
},
],
},
{
name: "reactComponentAnnotation",
fullDescription: `Options related to react component name annotations.
Disabled by default, unless a value is set for this option.
When enabled, your app's DOM will automatically be annotated during build-time with their respective component names.
This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring.
Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components
`,
supportedBundlers: ["webpack", "vite", "rollup"],
children: [
{
name: "enabled",
type: "boolean",
fullDescription: "Whether the component name annotate plugin should be enabled or not.",
supportedBundlers: ["webpack", "vite", "rollup"],
},
],
},
{
name: "_experiments",
type: "string",
Expand All @@ -351,17 +372,22 @@ type IncludeEntry = {
function generateTableOfContents(
depth: number,
parentId: string,
nodes: OptionDocumentation[]
nodes: OptionDocumentation[],
bundler: Bundler
): string {
return nodes
.map((node) => {
if (node.supportedBundlers && !node.supportedBundlers?.includes(bundler)) {
return "";
}

const id = `${parentId}-${node.name.toLowerCase()}`;
let output = `${" ".repeat(depth)}- [\`${node.name}\`](#${id
.replace(/-/g, "")
.toLowerCase()})`;
if (node.children && depth <= 0) {
output += "\n";
output += generateTableOfContents(depth + 1, id, node.children);
output += generateTableOfContents(depth + 1, id, node.children, bundler);
}
return output;
})
Expand All @@ -370,10 +396,15 @@ function generateTableOfContents(

function generateDescriptions(
parentName: string | undefined,
nodes: OptionDocumentation[]
nodes: OptionDocumentation[],
bundler: Bundler
): string {
return nodes
.map((node) => {
if (node.supportedBundlers && !node.supportedBundlers?.includes(bundler)) {
return "";
}

const name = parentName === undefined ? node.name : `${parentName}.${node.name}`;
let output = `### \`${name}\`

Expand All @@ -382,18 +413,18 @@ ${node.type === undefined ? "" : `Type: \`${node.type}\``}
${node.fullDescription}
`;
if (node.children) {
output += generateDescriptions(name, node.children);
output += generateDescriptions(name, node.children, bundler);
}
return output;
})
.join("\n");
}

export function generateOptionsDocumentation(): string {
export function generateOptionsDocumentation(bundler: Bundler): string {
return `## Options

${generateTableOfContents(0, "", options)}
${generateTableOfContents(0, "", options, bundler)}

${generateDescriptions(undefined, options)}
${generateDescriptions(undefined, options, bundler)}
`;
}
5 changes: 4 additions & 1 deletion packages/esbuild-plugin/src/prepack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import * as fs from "fs";
import * as path from "path";

const readmeTemplate = fs.readFileSync(path.join(__dirname, "..", "README_TEMPLATE.md"), "utf-8");
const readme = readmeTemplate.replace(/#OPTIONS_SECTION_INSERT#/, generateOptionsDocumentation());
const readme = readmeTemplate.replace(
/#OPTIONS_SECTION_INSERT#/,
generateOptionsDocumentation("esbuild")
);
fs.writeFileSync(path.join(__dirname, "..", "README.md"), readme, "utf-8");
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function checkBundle(bundlePath: string): void {
"webpack4",
"webpack5",
]) as string[],
depsVersions: { rollup: 3, vite: 3 },
depsVersions: { rollup: 3, vite: 3, react: 18 },
// This will differ based on what env this is run on
nodeVersion: expectedNodeVersion,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import childProcess from "child_process";
import path from "path";
import { testIfNodeMajorVersionIsLessThan18 } from "../../utils/testIf";

// prettier-ignore
const SNAPSHOT = `"<div><span data-sentry-component=\\"ComponentA\\" data-sentry-source-file=\\"component-a.jsx\\">Component A</span></div>"`
const ESBUILD_SNAPSHOT = `"<div><span>Component A</span></div>"`;

function checkBundle(bundlePath: string, snapshot = SNAPSHOT): void {
const processOutput = childProcess.execSync(`node ${bundlePath}`, { encoding: "utf-8" });
expect(processOutput.trim()).toMatchInlineSnapshot(snapshot);
}

test("esbuild bundle", () => {
expect.assertions(1);
checkBundle(path.join(__dirname, "./out/esbuild/index.js"), ESBUILD_SNAPSHOT);
});

test("rollup bundle", () => {
expect.assertions(1);
checkBundle(path.join(__dirname, "./out/rollup/index.js"));
});

test("vite bundle", () => {
expect.assertions(1);
checkBundle(path.join(__dirname, "./out/vite/index.js"));
});

testIfNodeMajorVersionIsLessThan18("webpack 4 bundle if node is < 18", () => {
expect.assertions(1);
checkBundle(path.join(__dirname, "./out/webpack4/index.js"));
});

test("webpack 5 bundle", () => {
expect.assertions(1);
checkBundle(path.join(__dirname, "./out/webpack5/index.js"));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { renderToString } from "react-dom/server";
import { ComponentA } from "./component-a";

export default function App() {
return <ComponentA />;
}

console.log(
renderToString(
<div>
<App />
</div>
)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function ComponentA() {
return <span>Component A</span>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as path from "path";
import { createCjsBundles } from "../../utils/create-cjs-bundles-for-react";

const entryPointPath = path.resolve(__dirname, "input", "app.jsx");
const outputDir = path.resolve(__dirname, "out");

createCjsBundles({ index: entryPointPath }, outputDir, {
telemetry: false,
reactComponentAnnotation: { enabled: true },
});
9 changes: 9 additions & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"clean:deps": "rimraf node_modules"
},
"dependencies": {
"@babel/preset-react": "^7.23.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@sentry-internal/eslint-config": "2.10.3",
"@sentry-internal/sentry-bundler-plugin-tsconfig": "2.10.3",
"@sentry/bundler-plugin-core": "2.10.3",
Expand All @@ -24,11 +28,16 @@
"@sentry/webpack-plugin": "2.10.3",
"@swc/jest": "^0.2.21",
"@types/jest": "^28.1.3",
"@types/react": "^18.2.0",
"@types/webpack4": "npm:@types/webpack@^4",
"@vitejs/plugin-react": "^4.2.1",
"babel-loader": "^8.0.0",
"esbuild": "0.14.49",
"esbuild019": "npm:esbuild@^0.19.4",
"eslint": "^8.18.0",
"jest": "^28.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "3.2.0",
"ts-node": "^10.9.1",
"vite": "3.0.0",
Expand Down
Loading