Applying Sentry Component Annotations Client-Side Only

Sentry's @sentry/babel-plugin-component-annotate injects data-sentry-component and data-sentry-source-file attributes onto every React component's root element. These attributes improve Sentry error stack traces and make it trivial to identify which component rendered which DOM node in the browser's web inspector:

<div
  data-sentry-component="PortfolioProjectEngagementSummary"
  data-sentry-source-file="PortfolioProjectEngagementSummary.tsx"
>
  ...
</div>

The problem is that these attributes are also injected during SSR, which means they're baked into every server-rendered HTML response. On contra.com, data-sentry-component alone accounted for ~28% of rendered HTML size.

Instead of adding the plugin to your global Babel config or @vitejs/plugin-react options, use a custom Vite plugin that checks options.ssr in the transform hook:

import * as babelCore from '@babel/core';
import type { PluginOption } from 'vite';
 
const sentryAnnotateClientOnly = (): PluginOption => ({
  enforce: 'pre',
  name: 'sentry-annotate-client-only',
  async transform(code, id, options) {
    if (options?.ssr) {
      return null;
    }
 
    const cleanId = id.split('?')[0];
 
    if (!/\.[jt]sx$/u.test(cleanId)) {
      return null;
    }
 
    if (cleanId.includes('node_modules')) {
      return null;
    }
 
    const result = await babelCore.transformAsync(code, {
      babelrc: false,
      configFile: false,
      filename: cleanId,
      parserOpts: { plugins: ['typescript', 'jsx'] },
      plugins: ['@sentry/babel-plugin-component-annotate'],
      sourceMaps: true,
    });
 
    if (!result?.code) {
      return null;
    }
 
    return { code: result.code, map: result.map };
  },
});

Then add it to your Vite config:

export default defineConfig({
  plugins: [
    sentryAnnotateClientOnly(),
    react(),
  ],
});

Vite's transform hook receives an options object with an ssr boolean. When Vite processes a module for the server bundle, options.ssr is true. By returning null early, the Babel plugin never runs on server-side code, so the annotations never appear in server-rendered HTML.

On the client side, the plugin runs normally. React hydration adds the attributes to the DOM during the first render pass, so Sentry error tracking works exactly the same.

A few details worth noting:

  • enforce: 'pre' ensures the plugin runs before other transforms, so it operates on the original JSX.
  • babelrc: false and configFile: false prevent Babel from picking up any project-level config, keeping this transform isolated.
  • sourceMaps: true preserves source map accuracy through the additional transform step.
  • We skip node_modules because annotating third-party components adds noise without useful debugging information.