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" ;
@@ -251,6 +256,7 @@ function functionBodyPushAttributes(
251256function processJSX (
252257 context : JSXProcessingContext ,
253258 jsxNode : Babel . NodePath ,
259+ isRoot = true ,
254260 componentName ?: string
255261) : void {
256262 if ( ! jsxNode ) {
@@ -265,11 +271,19 @@ function processJSX(
265271 const paths = jsxNode . get ( "openingElement" ) ;
266272 const openingElements = Array . isArray ( paths ) ? paths : [ paths ] ;
267273
274+ // Check if this is any type of fragment (shorthand <> or named <Fragment>)
275+ // If so, children stay at root; otherwise children are not root
276+ const firstOpeningElement = openingElements [ 0 ] as Babel . NodePath ;
277+ const isAnyFragment = jsxNode . isJSXFragment ( ) ||
278+ ( firstOpeningElement && isReactFragment ( context . t , firstOpeningElement , context . fragmentContext ) ) ;
279+ const isChildRoot = isAnyFragment && isRoot ;
280+
268281 openingElements . forEach ( ( openingElement ) => {
269282 applyAttributes (
270283 context ,
271284 openingElement as Babel . NodePath < Babel . types . JSXOpeningElement > ,
272- currentComponentName
285+ currentComponentName ,
286+ isRoot
273287 ) ;
274288 } ) ;
275289
@@ -288,7 +302,6 @@ function processJSX(
288302 return ;
289303 }
290304
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
292305 const openingElement = child . get ( "openingElement" ) ;
293306 // TODO: Improve this. We never expect to have multiple opening elements
294307 // but if it's possible, this should work
@@ -298,9 +311,10 @@ function processJSX(
298311
299312 if ( shouldSetComponentName && openingElement && openingElement . node ) {
300313 shouldSetComponentName = false ;
301- processJSX ( context , child , currentComponentName ) ;
314+ processJSX ( context , child , isChildRoot , currentComponentName ) ;
302315 } else {
303- processJSX ( context , child , "" ) ;
316+ // For fragments, children stay at root level; otherwise not root
317+ processJSX ( context , child , isChildRoot ) ;
304318 }
305319 } ) ;
306320}
@@ -309,11 +323,13 @@ function processJSX(
309323 * Applies Sentry tracking attributes to a JSX opening element.
310324 * Adds component name, element name, and source file attributes while
311325 * respecting ignore lists and fragment detection.
326+ * Only annotates HTML elements, not React components.
312327 */
313328function applyAttributes (
314329 context : JSXProcessingContext ,
315330 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
316- componentName : string
331+ componentName : string ,
332+ isRoot : boolean
317333) : void {
318334 const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context ;
319335 const [ componentAttributeName , elementAttributeName , sourceFileAttributeName ] = attributeNames ;
@@ -336,10 +352,18 @@ function applyAttributes(
336352 ( ignoredComponent ) => ignoredComponent === componentName || ignoredComponent === elementName
337353 ) ;
338354
355+ // Check if this is an HTML element (vs a React component)
356+ const isHtml = isHtmlElement ( elementName ) ;
357+
358+ // Skip annotation for React components - only annotate HTML elements
359+ if ( ! isHtml ) {
360+ return ;
361+ }
362+
339363 // Add a stable attribute for the element name but only for non-DOM names
340364 let isAnIgnoredElement = false ;
341365 if ( ! isAnIgnoredComponent && ! hasAttributeWithName ( openingElement , elementAttributeName ) ) {
342- if ( DEFAULT_IGNORED_ELEMENTS . includes ( elementName ) ) {
366+ if ( DEFAULT_HTML_ELEMENTS . includes ( elementName ) ) {
343367 isAnIgnoredElement = true ;
344368 } else {
345369 // Always add element attribute for non-ignored elements
@@ -351,10 +375,11 @@ function applyAttributes(
351375 }
352376 }
353377
354- // Add a stable attribute for the component name (absent for non- root elements)
378+ // Add component name to all root-level HTML elements
355379 if (
356380 componentName &&
357381 ! isAnIgnoredComponent &&
382+ isRoot &&
358383 ! hasAttributeWithName ( openingElement , componentAttributeName )
359384 ) {
360385 if ( componentAttributeName ) {
@@ -366,12 +391,12 @@ function applyAttributes(
366391
367392 // Add a stable attribute for the source file name
368393 // Updated condition: add source file for elements that have either:
369- // 1. A component name ( root elements) , OR
394+ // 1. At root level , OR
370395 // 2. An element name that's not ignored (child elements)
371396 if (
372397 sourceFileName &&
373398 ! isAnIgnoredComponent &&
374- ( componentName || ! isAnIgnoredElement ) &&
399+ ( isRoot || ! isAnIgnoredElement ) &&
375400 ! hasAttributeWithName ( openingElement , sourceFileAttributeName )
376401 ) {
377402 if ( sourceFileAttributeName ) {
@@ -593,6 +618,31 @@ function isReactFragment(
593618 return false ;
594619}
595620
621+ /**
622+ * Determines if an element is an HTML element (vs a React component)
623+ * HTML elements start with lowercase letters or are known React Native elements
624+ */
625+ function isHtmlElement ( elementName : string ) : boolean {
626+ // Unknown elements are not HTML elements
627+ if ( elementName === UNKNOWN_ELEMENT_NAME ) {
628+ return false ;
629+ }
630+
631+ // Check for lowercase first letter (standard HTML elements)
632+ if ( elementName . length > 0 && elementName . charAt ( 0 ) === elementName . charAt ( 0 ) . toLowerCase ( ) ) {
633+ return true ;
634+ }
635+
636+ // React Native elements typically start with uppercase but are still "native" elements
637+ // We consider them HTML-like elements for annotation purposes
638+ if ( REACT_NATIVE_ELEMENTS . includes ( elementName ) ) {
639+ return true ;
640+ }
641+
642+ // Otherwise, assume it's a React component (PascalCase)
643+ return false ;
644+ }
645+
596646function hasAttributeWithName (
597647 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
598648 name : string | undefined | null
0 commit comments