Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Automatically annotate React components at build time #464

Closed
wants to merge 78 commits into from

Conversation

0Calories
Copy link
Member

@0Calories 0Calories commented Jan 22, 2024

Adds an opt-in feature to the bundler plugins that will automatically add annotations to React components in JSX and TSX files, and their resulting HTML DOM nodes. This is done via an underlying Babel plugin that is run during build time. To enable the feature, set reactAnnotate.enabled to true.

Example of an automatically annotated component in Sentry's frontend:

image

This will allow the browser SDK to pick off component names and send them to Sentry, allowing for some very useful features:

  • React component names in Replays UI
  • React component name search / indexing in Replays
  • React component names in Breadcrumbs
  • React component names in interaction transactions, grouping spans by component name
image

The groundwork of the above features are already in place, now we just need to rollout this plugin to customers and observe! Adding it in the bundler plugins allows for an easy way to gain access to this feature, without requiring our customers to muck about in the build configs and manually add the Babel plugin.

This PR comes with integration tests, and has also been manually tested on all supported bundlers: Vite, ESBuild, Rollup, Webpack

Due to the experimental nature of this feature, it's opt in for now, but we eventually will want it enabled by default so as many customers as possible can get access to the capabilities that it unlocks. Of course, loader logic will need to be added where applicable so that performance is not impacted

@@ -52,6 +52,8 @@
"fix": "eslint ./src ./test --format stylish --fix"
},
"dependencies": {
"@babel/core": "7.18.5",
Copy link
Member Author

Choose a reason for hiding this comment

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

Had to move this to a regular dependency, since it is necessary to run transformAsync from @babel/core during the transform hook

0Calories and others added 21 commits January 23, 2024 10:44
- placeholder tests
- simple contrived "app"
- necessary test files
That way we don't have to including it in the package just for testing.
Instead of checking bundle output, run the output through a real React
renderer, and check its output against a snapshot. This makes more a
more honest integration test.
- add necessary Rollup build dependencies
- add spec and build pipeline
Compilation errors only show up in `stats`!
- add minimal viable Webpack 5 config
- add snapshot test
Version 9 is not compatible with Webpack 4
@0Calories 0Calories changed the title experiment(react-annotate): Automatically annotate React components at build time feat(core): Automatically annotate React components at build time Jan 29, 2024
@0Calories
Copy link
Member Author

0Calories commented Jan 29, 2024

In the process of fixing some lint errors and some more cleanup, but the bulk of this PR is done now. Opening it for review

@0Calories 0Calories marked this pull request as ready for review January 29, 2024 15:47
Log error separately, since its type is not known
Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

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

Okay initial review - but this PR needs to be split up to make it easier to review/understand.

  1. A PR that just vendors in packages/bundler-plugin-core/src/plugins/react-annotate-plugin.js, but makes it TS. Ideally we port any unit tests, or write ones as appropriate.
  2. A PR that adds react-annotate-plugin to core package, adds it to one of webpack/esbuild/vite/rollup, and adds e2e test accordingly
  3. A PR that adds plugin to all the rest of the plugins, and updates docs in the package (grep for OPTIONS_SECTION_INSERT and generateOptionsDocumentation() in the repo for docs info).

const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);

if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
return null;
Copy link
Member

Choose a reason for hiding this comment

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

m: is returning null safe here? Do we have to return { code } instead?

Copy link
Member Author

@0Calories 0Calories Jan 29, 2024

Choose a reason for hiding this comment

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

Good point, null is accepted by Unplugin as one of the return values of the transform hook, and I assumed it means that the file skips any transformations. However, I can't see any specific documentation on what returning null does in this hook

Copy link
Member Author

Choose a reason for hiding this comment

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

However, we already return null in our release injection plugin, under the transform hook for these same situations. Example: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/packages/bundler-plugin-core/src/index.ts#L388

@@ -510,6 +532,59 @@ export function createRollupDebugIdUploadHooks(
};
}

export function createReactAnnotateHooks() {
return {
async transform(code: string, id: string): Promise<TransformResult> {
Copy link
Member

Choose a reason for hiding this comment

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

m: can we add caching to this transform?

// 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\//)) {
Copy link
Member

Choose a reason for hiding this comment

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

m: Does this work with windows file paths?

Copy link
Member Author

Choose a reason for hiding this comment

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

This code is copied from another transform hook in our release injection plugin, so I assume so (but if not, it's broken in that plugin too :p)

}

if (name.indexOf("/") !== -1) {
return name.split("/").pop();
Copy link
Member

@AbhiPrasad AbhiPrasad Jan 29, 2024

Choose a reason for hiding this comment

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

l: IMO we should lift up magic constants into it's own variables.

* Options related to react component name annotations.
* Disabled by default, unless a value is set for this option
*/
reactAnnotate?: {
Copy link
Member

@AbhiPrasad AbhiPrasad Jan 29, 2024

Choose a reason for hiding this comment

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

l: Let's make this option name a bit more detailed

Suggested change
reactAnnotate?: {
reactComponentNameAnnotate?: {

Comment on lines +46 to +74
function esbuildReactAnnotatePlugin(): UnpluginOptions {
const pluginName = "sentry-esbuild-react-annotate-plugin";
return {
name: pluginName,
esbuild: {
setup({ onLoad }) {
onLoad({ filter: /\.(t|j)sx$/ }, async (args) => {
const code = await fsPromises.readFile(args.path, "utf8");
const loader = args.path.endsWith(".tsx") ? "tsx" : "jsx";
const results = await createReactAnnotateHooks().transform(code, args.path);

let contents: string = code;

if (typeof results === "string") {
contents = results;
} else if (results?.code) {
contents = results.code;
}

return {
loader,
pluginName,
contents,
};
});
},
},
};
}
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, we cannot use a simple onLoad hook for this funcitonality esbuild plugin, and in fact, I believe we cannot do this with good conscience in esbuild at all. Esbuild doesn't have a transform hook and that is by design. Read this comment I wrote for a similar situation: #428 (comment)

I don't feel like pressing the "changes requested" button but please consider this as a pretty hard blocker!

Copy link
Member Author

Choose a reason for hiding this comment

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

Hey Luca, thanks for letting me know. I see, that's quite unfortunate that we can't use onLoad like this :/
Would you know of any alternatives that we can use to support this functionality in esbuild?

I assume the biggest problem here is that the onLoad hook only loads in files once, so this will prevent other loaders from running on these same files. In that case, can we fix this by using a namespace?

The esbuild docs says this:

A callback added using onLoad will be run for each unique path/namespace pair that has not been marked as external.

It sounds like as long as we assign a unique namespace during the onResolve hook, we can get around this. Please lmk if this is an acceptable solution :)

Copy link
Member Author

Choose a reason for hiding this comment

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

The plugin should not add much computational overhead as well, since it is an O(n) time algorithm. All it's doing is parsing JSX files and adding additional properties onto JSX elements

Copy link
Member

Choose a reason for hiding this comment

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

I honestly do not think you can do this in esbuild with good conscience. If you assign a different namespace to the files in onResolve you will prevent the file from being picked up by other plugins' onLoad hooks and breaking them. This might be fine in a lot of cases but as soon as people add their own onLoad hooks this will break. I'll leave it up to you but just so you're aware.

Copy link
Member Author

Choose a reason for hiding this comment

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

Understood, in that case we will hold off on releasing this plugin to esbuild for now, but we can reconsider it in the future if customers ask for it 👍

@0Calories
Copy link
Member Author

Closing this PR, I'm currently in the process of breaking it down into incremental PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants