3030 * - Added `sentry` to data properties, i.e `data-sentry-component`
3131 * - Converted to TypeScript
3232 * - Code cleanups
33+ * - Inject into root HTML elements instead of React component
3334 */
3435
3536import type * as Babel from "@babel/core" ;
3637import type { PluginObj , PluginPass } from "@babel/core" ;
3738
38- import { DEFAULT_IGNORED_ELEMENTS , KNOWN_INCOMPATIBLE_PLUGINS } from "./constants" ;
39+ import {
40+ DEFAULT_HTML_ELEMENTS ,
41+ KNOWN_INCOMPATIBLE_PLUGINS ,
42+ REACT_NATIVE_ELEMENTS ,
43+ } from "./constants" ;
3944
4045const webComponentName = "data-sentry-component" ;
4146const webElementName = "data-sentry-element" ;
@@ -100,7 +105,7 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
100105 }
101106
102107 const context = createJSXProcessingContext ( state , t , path . node . id . name ) ;
103- functionBodyPushAttributes ( context , path ) ;
108+ functionBodyPushAttributes ( context , path , true ) ;
104109 } ,
105110 ArrowFunctionExpression ( path , state ) {
106111 // We're expecting a `VariableDeclarator` like `const MyComponent =`
@@ -121,7 +126,7 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
121126 }
122127
123128 const context = createJSXProcessingContext ( state , t , parent . id . name ) ;
124- functionBodyPushAttributes ( context , path ) ;
129+ functionBodyPushAttributes ( context , path , true ) ;
125130 } ,
126131 ClassDeclaration ( path , state ) {
127132 const name = path . get ( "id" ) ;
@@ -178,7 +183,8 @@ function createJSXProcessingContext(
178183 */
179184function functionBodyPushAttributes (
180185 context : JSXProcessingContext ,
181- path : Babel . NodePath < Babel . types . Function >
186+ path : Babel . NodePath < Babel . types . Function > ,
187+ isRoot : boolean
182188) : void {
183189 let jsxNode : Babel . NodePath ;
184190
@@ -220,11 +226,11 @@ function functionBodyPushAttributes(
220226 if ( arg . isConditionalExpression ( ) ) {
221227 const consequent = arg . get ( "consequent" ) ;
222228 if ( consequent . isJSXFragment ( ) || consequent . isJSXElement ( ) ) {
223- processJSX ( context , consequent ) ;
229+ processJSX ( context , consequent , isRoot ) ;
224230 }
225231 const alternate = arg . get ( "alternate" ) ;
226232 if ( alternate . isJSXFragment ( ) || alternate . isJSXElement ( ) ) {
227- processJSX ( context , alternate ) ;
233+ processJSX ( context , alternate , isRoot ) ;
228234 }
229235 return ;
230236 }
@@ -240,7 +246,7 @@ function functionBodyPushAttributes(
240246 return ;
241247 }
242248
243- processJSX ( context , jsxNode ) ;
249+ processJSX ( context , jsxNode , isRoot ) ;
244250}
245251
246252/**
@@ -251,6 +257,7 @@ function functionBodyPushAttributes(
251257function processJSX (
252258 context : JSXProcessingContext ,
253259 jsxNode : Babel . NodePath ,
260+ isRoot = true ,
254261 componentName ?: string
255262) : void {
256263 if ( ! jsxNode ) {
@@ -265,11 +272,15 @@ function processJSX(
265272 const paths = jsxNode . get ( "openingElement" ) ;
266273 const openingElements = Array . isArray ( paths ) ? paths : [ paths ] ;
267274
275+ // Check if this is a fragment - if so, children stay at root; otherwise children are not root
276+ const isChildRoot = jsxNode . isJSXFragment ( ) && isRoot ;
277+
268278 openingElements . forEach ( ( openingElement ) => {
269279 applyAttributes (
270280 context ,
271281 openingElement as Babel . NodePath < Babel . types . JSXOpeningElement > ,
272- currentComponentName
282+ currentComponentName ,
283+ isRoot
273284 ) ;
274285 } ) ;
275286
@@ -288,7 +299,6 @@ function processJSX(
288299 return ;
289300 }
290301
291- // Children don't receive the data-component attribute so we pass null for componentName unless it's the first child of a Fragment with a node and `annotateFragments` is true
292302 const openingElement = child . get ( "openingElement" ) ;
293303 // TODO: Improve this. We never expect to have multiple opening elements
294304 // but if it's possible, this should work
@@ -298,9 +308,10 @@ function processJSX(
298308
299309 if ( shouldSetComponentName && openingElement && openingElement . node ) {
300310 shouldSetComponentName = false ;
301- processJSX ( context , child , currentComponentName ) ;
311+ processJSX ( context , child , isChildRoot , currentComponentName ) ;
302312 } else {
303- processJSX ( context , child , "" ) ;
313+ // For fragments, children stay at root level; otherwise not root
314+ processJSX ( context , child , isChildRoot , currentComponentName ) ;
304315 }
305316 } ) ;
306317}
@@ -309,11 +320,13 @@ function processJSX(
309320 * Applies Sentry tracking attributes to a JSX opening element.
310321 * Adds component name, element name, and source file attributes while
311322 * respecting ignore lists and fragment detection.
323+ * Only annotates HTML elements, not React components.
312324 */
313325function applyAttributes (
314326 context : JSXProcessingContext ,
315327 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
316- componentName : string
328+ componentName : string ,
329+ isRoot : boolean
317330) : void {
318331 const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context ;
319332 const [ componentAttributeName , elementAttributeName , sourceFileAttributeName ] = attributeNames ;
@@ -336,10 +349,18 @@ function applyAttributes(
336349 ( ignoredComponent ) => ignoredComponent === componentName || ignoredComponent === elementName
337350 ) ;
338351
352+ // Check if this is an HTML element (vs a React component)
353+ const isHtml = isHtmlElement ( elementName ) ;
354+
355+ // Skip annotation for React components - only annotate HTML elements
356+ if ( ! isHtml ) {
357+ return ;
358+ }
359+
339360 // Add a stable attribute for the element name but only for non-DOM names
340361 let isAnIgnoredElement = false ;
341362 if ( ! isAnIgnoredComponent && ! hasAttributeWithName ( openingElement , elementAttributeName ) ) {
342- if ( DEFAULT_IGNORED_ELEMENTS . includes ( elementName ) ) {
363+ if ( DEFAULT_HTML_ELEMENTS . includes ( elementName ) ) {
343364 isAnIgnoredElement = true ;
344365 } else {
345366 // Always add element attribute for non-ignored elements
@@ -351,10 +372,11 @@ function applyAttributes(
351372 }
352373 }
353374
354- // Add a stable attribute for the component name (absent for non- root elements)
375+ // Add component name to all root-level HTML elements
355376 if (
356377 componentName &&
357378 ! isAnIgnoredComponent &&
379+ isRoot &&
358380 ! hasAttributeWithName ( openingElement , componentAttributeName )
359381 ) {
360382 if ( componentAttributeName ) {
@@ -366,12 +388,12 @@ function applyAttributes(
366388
367389 // Add a stable attribute for the source file name
368390 // Updated condition: add source file for elements that have either:
369- // 1. A component name ( root elements) , OR
391+ // 1. At root level , OR
370392 // 2. An element name that's not ignored (child elements)
371393 if (
372394 sourceFileName &&
373395 ! isAnIgnoredComponent &&
374- ( componentName || ! isAnIgnoredElement ) &&
396+ ( isRoot || ! isAnIgnoredElement ) &&
375397 ! hasAttributeWithName ( openingElement , sourceFileAttributeName )
376398 ) {
377399 if ( sourceFileAttributeName ) {
@@ -593,6 +615,31 @@ function isReactFragment(
593615 return false ;
594616}
595617
618+ /**
619+ * Determines if an element is an HTML element (vs a React component)
620+ * HTML elements start with lowercase letters or are known React Native elements
621+ */
622+ function isHtmlElement ( elementName : string ) : boolean {
623+ // Unknown elements are not HTML elements
624+ if ( elementName === UNKNOWN_ELEMENT_NAME ) {
625+ return false ;
626+ }
627+
628+ // Check for lowercase first letter (standard HTML elements)
629+ if ( elementName . length > 0 && elementName . charAt ( 0 ) === elementName . charAt ( 0 ) . toLowerCase ( ) ) {
630+ return true ;
631+ }
632+
633+ // React Native elements typically start with uppercase but are still "native" elements
634+ // We consider them HTML-like elements for annotation purposes
635+ if ( REACT_NATIVE_ELEMENTS . includes ( elementName ) ) {
636+ return true ;
637+ }
638+
639+ // Otherwise, assume it's a React component (PascalCase)
640+ return false ;
641+ }
642+
596643function hasAttributeWithName (
597644 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
598645 name : string | undefined | null
0 commit comments