Skip to content

Commit fba3468

Browse files
authored
feat(core): Include component name annotation plugin with all bundler plugins except esbuild (#469)
* Add hook function for annotate plugin in core index * Add options and typing * Add to rollup, vite, webpack * Allow conditional rendering of options in docs based on supported bundlers
1 parent 4d123db commit fba3468

File tree

21 files changed

+819
-30
lines changed

21 files changed

+819
-30
lines changed

packages/bundler-plugin-core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,18 @@
5252
"fix": "eslint ./src ./test --format stylish --fix"
5353
},
5454
"dependencies": {
55+
"@babel/core": "7.18.5",
5556
"@sentry/cli": "^2.22.3",
5657
"@sentry/node": "^7.60.0",
5758
"@sentry/utils": "^7.60.0",
59+
"@sentry/component-annotate-plugin": "2.10.3",
5860
"dotenv": "^16.3.1",
5961
"find-up": "5.0.0",
6062
"glob": "9.3.2",
6163
"magic-string": "0.27.0",
6264
"unplugin": "1.0.1"
6365
},
6466
"devDependencies": {
65-
"@babel/core": "7.18.5",
6667
"@babel/preset-env": "7.18.2",
6768
"@babel/preset-typescript": "7.17.12",
6869
"@rollup/plugin-babel": "5.3.1",

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

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import SentryCli from "@sentry/cli";
2+
import { transformAsync } from "@babel/core";
3+
import { componentNameAnnotatePlugin } from "@sentry/component-annotate-plugin";
24
import * as fs from "fs";
35
import * as path from "path";
46
import MagicString from "magic-string";
5-
import { createUnplugin, UnpluginOptions } from "unplugin";
7+
import { createUnplugin, TransformResult, UnpluginOptions } from "unplugin";
68
import { normalizeUserOptions, validateOptions } from "./options-mapping";
79
import { createDebugIdUploadFunction } from "./debug-id-upload";
810
import { releaseManagementPlugin } from "./plugins/release-management";
@@ -22,9 +24,12 @@ import {
2224
} from "./utils";
2325
import * as dotenv from "dotenv";
2426
import { glob } from "glob";
27+
import pkg from "@sentry/utils";
28+
const { logger } = pkg;
2529

2630
interface SentryUnpluginFactoryOptions {
2731
releaseInjectionPlugin: (injectionCode: string) => UnpluginOptions;
32+
componentNameAnnotatePlugin?: () => UnpluginOptions;
2833
moduleMetadataInjectionPlugin?: (injectionCode: string) => UnpluginOptions;
2934
debugIdInjectionPlugin: () => UnpluginOptions;
3035
debugIdUploadPlugin: (upload: (buildArtifacts: string[]) => Promise<void>) => UnpluginOptions;
@@ -60,6 +65,7 @@ interface SentryUnpluginFactoryOptions {
6065
*/
6166
export function sentryUnpluginFactory({
6267
releaseInjectionPlugin,
68+
componentNameAnnotatePlugin,
6369
moduleMetadataInjectionPlugin,
6470
debugIdInjectionPlugin,
6571
debugIdUploadPlugin,
@@ -317,6 +323,20 @@ export function sentryUnpluginFactory({
317323
);
318324
}
319325

326+
if (options.reactComponentAnnotation) {
327+
if (!options.reactComponentAnnotation.enabled) {
328+
logger.info(
329+
"The component name annotate plugin is currently disabled. Skipping component name annotations."
330+
);
331+
} else if (options.reactComponentAnnotation.enabled && !componentNameAnnotatePlugin) {
332+
logger.warn(
333+
"The component name annotate plugin is currently not supported by '@sentry/esbuild-plugin'"
334+
);
335+
} else {
336+
componentNameAnnotatePlugin && plugins.push(componentNameAnnotatePlugin());
337+
}
338+
}
339+
320340
return plugins;
321341
});
322342
}
@@ -346,7 +366,6 @@ export function sentryCliBinaryExists(): boolean {
346366

347367
export function createRollupReleaseInjectionHooks(injectionCode: string) {
348368
const virtualReleaseInjectionFileId = "\0sentry-release-injection-file";
349-
350369
return {
351370
resolveId(id: string) {
352371
if (id === virtualReleaseInjectionFileId) {
@@ -510,6 +529,60 @@ export function createRollupDebugIdUploadHooks(
510529
};
511530
}
512531

532+
export function createComponentNameAnnotateHooks() {
533+
type ParserPlugins = NonNullable<
534+
NonNullable<Parameters<typeof transformAsync>[1]>["parserOpts"]
535+
>["plugins"];
536+
537+
return {
538+
async transform(this: void, code: string, id: string): Promise<TransformResult> {
539+
// id may contain query and hash which will trip up our file extension logic below
540+
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
541+
542+
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
543+
return null;
544+
}
545+
546+
// We will only apply this plugin on jsx and tsx files
547+
if (![".jsx", ".tsx"].some((ending) => idWithoutQueryAndHash.endsWith(ending))) {
548+
return null;
549+
}
550+
551+
const parserPlugins: ParserPlugins = [];
552+
if (idWithoutQueryAndHash.endsWith(".jsx")) {
553+
parserPlugins.push("jsx");
554+
} else if (idWithoutQueryAndHash.endsWith(".tsx")) {
555+
parserPlugins.push("jsx", "typescript");
556+
}
557+
558+
try {
559+
const result = await transformAsync(code, {
560+
plugins: [[componentNameAnnotatePlugin]],
561+
filename: id,
562+
parserOpts: {
563+
sourceType: "module",
564+
allowAwaitOutsideFunction: true,
565+
plugins: parserPlugins,
566+
},
567+
generatorOpts: {
568+
decoratorsBeforeExport: true,
569+
},
570+
sourceMaps: true,
571+
});
572+
573+
return {
574+
code: result?.code ?? code,
575+
map: result?.map,
576+
};
577+
} catch (e) {
578+
logger.error(`Failed to apply react annotate plugin`, e);
579+
}
580+
581+
return { code };
582+
},
583+
};
584+
}
585+
513586
export function getDebugIdSnippet(debugId: string): string {
514587
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){}}();`;
515588
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function normalizeUserOptions(userOptions: UserOptions) {
2929
cleanArtifacts: userOptions.release?.cleanArtifacts ?? false,
3030
},
3131
bundleSizeOptimizations: userOptions.bundleSizeOptimizations,
32+
reactComponentAnnotation: userOptions.reactComponentAnnotation,
3233
_experiments: userOptions._experiments ?? {},
3334
};
3435

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,20 @@ export interface Options {
278278
excludeReplayWorker?: boolean;
279279
};
280280

281+
/**
282+
* Options related to react component name annotations.
283+
* Disabled by default, unless a value is set for this option.
284+
* When enabled, your app's DOM will automatically be annotated during build-time with their respective component names.
285+
* 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.
286+
* Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components
287+
*/
288+
reactComponentAnnotation?: {
289+
/**
290+
* Whether the component name annotate plugin should be enabled or not.
291+
*/
292+
enabled?: boolean;
293+
};
294+
281295
/**
282296
* Options that are considered experimental and subject to change.
283297
*

packages/component-annotate-plugin/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@
4747
"clean:build": "rimraf ./dist *.tgz",
4848
"clean:deps": "rimraf node_modules",
4949
"test": "jest",
50-
"lint": "eslint ./src ./test",
51-
"prepack": "ts-node ./src/prepack.ts"
50+
"lint": "eslint ./src ./test"
5251
},
5352
"dependencies": {},
5453
"devDependencies": {

packages/dev-utils/src/generate-documentation-table.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
type Bundler = "webpack" | "vite" | "rollup" | "esbuild";
2+
13
type OptionDocumentation = {
24
name: string;
35
fullDescription: string;
46
type?: string;
57
children?: OptionDocumentation[];
8+
supportedBundlers?: Bundler[];
69
};
710

811
const options: OptionDocumentation[] = [
@@ -332,6 +335,24 @@ type IncludeEntry = {
332335
},
333336
],
334337
},
338+
{
339+
name: "reactComponentAnnotation",
340+
fullDescription: `Options related to react component name annotations.
341+
Disabled by default, unless a value is set for this option.
342+
When enabled, your app's DOM will automatically be annotated during build-time with their respective component names.
343+
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.
344+
Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components
345+
`,
346+
supportedBundlers: ["webpack", "vite", "rollup"],
347+
children: [
348+
{
349+
name: "enabled",
350+
type: "boolean",
351+
fullDescription: "Whether the component name annotate plugin should be enabled or not.",
352+
supportedBundlers: ["webpack", "vite", "rollup"],
353+
},
354+
],
355+
},
335356
{
336357
name: "_experiments",
337358
type: "string",
@@ -351,17 +372,22 @@ type IncludeEntry = {
351372
function generateTableOfContents(
352373
depth: number,
353374
parentId: string,
354-
nodes: OptionDocumentation[]
375+
nodes: OptionDocumentation[],
376+
bundler: Bundler
355377
): string {
356378
return nodes
357379
.map((node) => {
380+
if (node.supportedBundlers && !node.supportedBundlers?.includes(bundler)) {
381+
return "";
382+
}
383+
358384
const id = `${parentId}-${node.name.toLowerCase()}`;
359385
let output = `${" ".repeat(depth)}- [\`${node.name}\`](#${id
360386
.replace(/-/g, "")
361387
.toLowerCase()})`;
362388
if (node.children && depth <= 0) {
363389
output += "\n";
364-
output += generateTableOfContents(depth + 1, id, node.children);
390+
output += generateTableOfContents(depth + 1, id, node.children, bundler);
365391
}
366392
return output;
367393
})
@@ -370,10 +396,15 @@ function generateTableOfContents(
370396

371397
function generateDescriptions(
372398
parentName: string | undefined,
373-
nodes: OptionDocumentation[]
399+
nodes: OptionDocumentation[],
400+
bundler: Bundler
374401
): string {
375402
return nodes
376403
.map((node) => {
404+
if (node.supportedBundlers && !node.supportedBundlers?.includes(bundler)) {
405+
return "";
406+
}
407+
377408
const name = parentName === undefined ? node.name : `${parentName}.${node.name}`;
378409
let output = `### \`${name}\`
379410
@@ -382,18 +413,18 @@ ${node.type === undefined ? "" : `Type: \`${node.type}\``}
382413
${node.fullDescription}
383414
`;
384415
if (node.children) {
385-
output += generateDescriptions(name, node.children);
416+
output += generateDescriptions(name, node.children, bundler);
386417
}
387418
return output;
388419
})
389420
.join("\n");
390421
}
391422

392-
export function generateOptionsDocumentation(): string {
423+
export function generateOptionsDocumentation(bundler: Bundler): string {
393424
return `## Options
394425
395-
${generateTableOfContents(0, "", options)}
426+
${generateTableOfContents(0, "", options, bundler)}
396427
397-
${generateDescriptions(undefined, options)}
428+
${generateDescriptions(undefined, options, bundler)}
398429
`;
399430
}

packages/esbuild-plugin/src/prepack.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ import * as fs from "fs";
33
import * as path from "path";
44

55
const readmeTemplate = fs.readFileSync(path.join(__dirname, "..", "README_TEMPLATE.md"), "utf-8");
6-
const readme = readmeTemplate.replace(/#OPTIONS_SECTION_INSERT#/, generateOptionsDocumentation());
6+
const readme = readmeTemplate.replace(
7+
/#OPTIONS_SECTION_INSERT#/,
8+
generateOptionsDocumentation("esbuild")
9+
);
710
fs.writeFileSync(path.join(__dirname, "..", "README.md"), readme, "utf-8");

packages/integration-tests/fixtures/build-information-injection/build-information-injection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function checkBundle(bundlePath: string): void {
2222
"webpack4",
2323
"webpack5",
2424
]) as string[],
25-
depsVersions: { rollup: 3, vite: 3 },
25+
depsVersions: { rollup: 3, vite: 3, react: 18 },
2626
// This will differ based on what env this is run on
2727
nodeVersion: expectedNodeVersion,
2828
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import childProcess from "child_process";
2+
import path from "path";
3+
import { testIfNodeMajorVersionIsLessThan18 } from "../../utils/testIf";
4+
5+
// prettier-ignore
6+
const SNAPSHOT = `"<div><span data-sentry-component=\\"ComponentA\\" data-sentry-source-file=\\"component-a.jsx\\">Component A</span></div>"`
7+
const ESBUILD_SNAPSHOT = `"<div><span>Component A</span></div>"`;
8+
9+
function checkBundle(bundlePath: string, snapshot = SNAPSHOT): void {
10+
const processOutput = childProcess.execSync(`node ${bundlePath}`, { encoding: "utf-8" });
11+
expect(processOutput.trim()).toMatchInlineSnapshot(snapshot);
12+
}
13+
14+
test("esbuild bundle", () => {
15+
expect.assertions(1);
16+
checkBundle(path.join(__dirname, "./out/esbuild/index.js"), ESBUILD_SNAPSHOT);
17+
});
18+
19+
test("rollup bundle", () => {
20+
expect.assertions(1);
21+
checkBundle(path.join(__dirname, "./out/rollup/index.js"));
22+
});
23+
24+
test("vite bundle", () => {
25+
expect.assertions(1);
26+
checkBundle(path.join(__dirname, "./out/vite/index.js"));
27+
});
28+
29+
testIfNodeMajorVersionIsLessThan18("webpack 4 bundle if node is < 18", () => {
30+
expect.assertions(1);
31+
checkBundle(path.join(__dirname, "./out/webpack4/index.js"));
32+
});
33+
34+
test("webpack 5 bundle", () => {
35+
expect.assertions(1);
36+
checkBundle(path.join(__dirname, "./out/webpack5/index.js"));
37+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { renderToString } from "react-dom/server";
2+
import { ComponentA } from "./component-a";
3+
4+
export default function App() {
5+
return <ComponentA />;
6+
}
7+
8+
console.log(
9+
renderToString(
10+
<div>
11+
<App />
12+
</div>
13+
)
14+
);

0 commit comments

Comments
 (0)