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,21 @@ 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 =
278+ jsxNode . isJSXFragment ( ) ||
279+ ( firstOpeningElement &&
280+ isReactFragment ( context . t , firstOpeningElement , context . fragmentContext ) ) ;
281+ const isChildRoot = isAnyFragment && isRoot ;
282+
268283 openingElements . forEach ( ( openingElement ) => {
269284 applyAttributes (
270285 context ,
271286 openingElement as Babel . NodePath < Babel . types . JSXOpeningElement > ,
272- currentComponentName
287+ currentComponentName ,
288+ isRoot
273289 ) ;
274290 } ) ;
275291
@@ -288,7 +304,6 @@ function processJSX(
288304 return ;
289305 }
290306
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
292307 const openingElement = child . get ( "openingElement" ) ;
293308 // TODO: Improve this. We never expect to have multiple opening elements
294309 // but if it's possible, this should work
@@ -298,9 +313,10 @@ function processJSX(
298313
299314 if ( shouldSetComponentName && openingElement && openingElement . node ) {
300315 shouldSetComponentName = false ;
301- processJSX ( context , child , currentComponentName ) ;
316+ processJSX ( context , child , isChildRoot , currentComponentName ) ;
302317 } else {
303- processJSX ( context , child , "" ) ;
318+ // For fragments, children stay at root level; otherwise not root
319+ processJSX ( context , child , isChildRoot ) ;
304320 }
305321 } ) ;
306322}
@@ -309,11 +325,13 @@ function processJSX(
309325 * Applies Sentry tracking attributes to a JSX opening element.
310326 * Adds component name, element name, and source file attributes while
311327 * respecting ignore lists and fragment detection.
328+ * Only annotates HTML elements, not React components.
312329 */
313330function applyAttributes (
314331 context : JSXProcessingContext ,
315332 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
316- componentName : string
333+ componentName : string ,
334+ isRoot : boolean
317335) : void {
318336 const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context ;
319337 const [ componentAttributeName , elementAttributeName , sourceFileAttributeName ] = attributeNames ;
@@ -336,10 +354,18 @@ function applyAttributes(
336354 ( ignoredComponent ) => ignoredComponent === componentName || ignoredComponent === elementName
337355 ) ;
338356
357+ // Check if this is an HTML element (vs a React component)
358+ const isHtml = isHtmlElement ( elementName ) ;
359+
360+ // Skip annotation for React components - only annotate HTML elements
361+ if ( ! isHtml ) {
362+ return ;
363+ }
364+
339365 // Add a stable attribute for the element name but only for non-DOM names
340366 let isAnIgnoredElement = false ;
341367 if ( ! isAnIgnoredComponent && ! hasAttributeWithName ( openingElement , elementAttributeName ) ) {
342- if ( DEFAULT_IGNORED_ELEMENTS . includes ( elementName ) ) {
368+ if ( DEFAULT_HTML_ELEMENTS . includes ( elementName ) ) {
343369 isAnIgnoredElement = true ;
344370 } else {
345371 // Always add element attribute for non-ignored elements
@@ -351,10 +377,11 @@ function applyAttributes(
351377 }
352378 }
353379
354- // Add a stable attribute for the component name (absent for non- root elements)
380+ // Add component name to all root-level HTML elements
355381 if (
356382 componentName &&
357383 ! isAnIgnoredComponent &&
384+ isRoot &&
358385 ! hasAttributeWithName ( openingElement , componentAttributeName )
359386 ) {
360387 if ( componentAttributeName ) {
@@ -366,12 +393,12 @@ function applyAttributes(
366393
367394 // Add a stable attribute for the source file name
368395 // Updated condition: add source file for elements that have either:
369- // 1. A component name ( root elements) , OR
396+ // 1. At root level , OR
370397 // 2. An element name that's not ignored (child elements)
371398 if (
372399 sourceFileName &&
373400 ! isAnIgnoredComponent &&
374- ( componentName || ! isAnIgnoredElement ) &&
401+ ( isRoot || ! isAnIgnoredElement ) &&
375402 ! hasAttributeWithName ( openingElement , sourceFileAttributeName )
376403 ) {
377404 if ( sourceFileAttributeName ) {
@@ -593,6 +620,31 @@ function isReactFragment(
593620 return false ;
594621}
595622
623+ /**
624+ * Determines if an element is an HTML element (vs a React component)
625+ * HTML elements start with lowercase letters or are known React Native elements
626+ */
627+ function isHtmlElement ( elementName : string ) : boolean {
628+ // Unknown elements are not HTML elements
629+ if ( elementName === UNKNOWN_ELEMENT_NAME ) {
630+ return false ;
631+ }
632+
633+ // Check for lowercase first letter (standard HTML elements)
634+ if ( elementName . length > 0 && elementName . charAt ( 0 ) === elementName . charAt ( 0 ) . toLowerCase ( ) ) {
635+ return true ;
636+ }
637+
638+ // React Native elements typically start with uppercase but are still "native" elements
639+ // We consider them HTML-like elements for annotation purposes
640+ if ( REACT_NATIVE_ELEMENTS . includes ( elementName ) ) {
641+ return true ;
642+ }
643+
644+ // Otherwise, assume it's a React component (PascalCase)
645+ return false ;
646+ }
647+
596648function hasAttributeWithName (
597649 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
598650 name : string | undefined | null
0 commit comments