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,15 @@ function processJSX(
265271 const paths = jsxNode . get ( "openingElement" ) ;
266272 const openingElements = Array . isArray ( paths ) ? paths : [ paths ] ;
267273
274+ // Check if this is a fragment - if so, children stay at root; otherwise children are not root
275+ const isChildRoot = jsxNode . isJSXFragment ( ) && isRoot ;
276+
268277 openingElements . forEach ( ( openingElement ) => {
269278 applyAttributes (
270279 context ,
271280 openingElement as Babel . NodePath < Babel . types . JSXOpeningElement > ,
272- currentComponentName
281+ currentComponentName ,
282+ isRoot
273283 ) ;
274284 } ) ;
275285
@@ -288,7 +298,6 @@ function processJSX(
288298 return ;
289299 }
290300
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
292301 const openingElement = child . get ( "openingElement" ) ;
293302 // TODO: Improve this. We never expect to have multiple opening elements
294303 // but if it's possible, this should work
@@ -298,9 +307,10 @@ function processJSX(
298307
299308 if ( shouldSetComponentName && openingElement && openingElement . node ) {
300309 shouldSetComponentName = false ;
301- processJSX ( context , child , currentComponentName ) ;
310+ processJSX ( context , child , isChildRoot , currentComponentName ) ;
302311 } else {
303- processJSX ( context , child , "" ) ;
312+ // For fragments, children stay at root level; otherwise not root
313+ processJSX ( context , child , isChildRoot , currentComponentName ) ;
304314 }
305315 } ) ;
306316}
@@ -309,11 +319,13 @@ function processJSX(
309319 * Applies Sentry tracking attributes to a JSX opening element.
310320 * Adds component name, element name, and source file attributes while
311321 * respecting ignore lists and fragment detection.
322+ * Only annotates HTML elements, not React components.
312323 */
313324function applyAttributes (
314325 context : JSXProcessingContext ,
315326 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
316- componentName : string
327+ componentName : string ,
328+ isRoot : boolean
317329) : void {
318330 const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context ;
319331 const [ componentAttributeName , elementAttributeName , sourceFileAttributeName ] = attributeNames ;
@@ -336,10 +348,18 @@ function applyAttributes(
336348 ( ignoredComponent ) => ignoredComponent === componentName || ignoredComponent === elementName
337349 ) ;
338350
351+ // Check if this is an HTML element (vs a React component)
352+ const isHtml = isHtmlElement ( elementName ) ;
353+
354+ // Skip annotation for React components - only annotate HTML elements
355+ if ( ! isHtml ) {
356+ return ;
357+ }
358+
339359 // Add a stable attribute for the element name but only for non-DOM names
340360 let isAnIgnoredElement = false ;
341361 if ( ! isAnIgnoredComponent && ! hasAttributeWithName ( openingElement , elementAttributeName ) ) {
342- if ( DEFAULT_IGNORED_ELEMENTS . includes ( elementName ) ) {
362+ if ( DEFAULT_HTML_ELEMENTS . includes ( elementName ) ) {
343363 isAnIgnoredElement = true ;
344364 } else {
345365 // Always add element attribute for non-ignored elements
@@ -351,10 +371,11 @@ function applyAttributes(
351371 }
352372 }
353373
354- // Add a stable attribute for the component name (absent for non- root elements)
374+ // Add component name to all root-level HTML elements
355375 if (
356376 componentName &&
357377 ! isAnIgnoredComponent &&
378+ isRoot &&
358379 ! hasAttributeWithName ( openingElement , componentAttributeName )
359380 ) {
360381 if ( componentAttributeName ) {
@@ -366,12 +387,12 @@ function applyAttributes(
366387
367388 // Add a stable attribute for the source file name
368389 // Updated condition: add source file for elements that have either:
369- // 1. A component name ( root elements) , OR
390+ // 1. At root level , OR
370391 // 2. An element name that's not ignored (child elements)
371392 if (
372393 sourceFileName &&
373394 ! isAnIgnoredComponent &&
374- ( componentName || ! isAnIgnoredElement ) &&
395+ ( isRoot || ! isAnIgnoredElement ) &&
375396 ! hasAttributeWithName ( openingElement , sourceFileAttributeName )
376397 ) {
377398 if ( sourceFileAttributeName ) {
@@ -593,6 +614,31 @@ function isReactFragment(
593614 return false ;
594615}
595616
617+ /**
618+ * Determines if an element is an HTML element (vs a React component)
619+ * HTML elements start with lowercase letters or are known React Native elements
620+ */
621+ function isHtmlElement ( elementName : string ) : boolean {
622+ // Unknown elements are not HTML elements
623+ if ( elementName === UNKNOWN_ELEMENT_NAME ) {
624+ return false ;
625+ }
626+
627+ // Check for lowercase first letter (standard HTML elements)
628+ if ( elementName . length > 0 && elementName . charAt ( 0 ) === elementName . charAt ( 0 ) . toLowerCase ( ) ) {
629+ return true ;
630+ }
631+
632+ // React Native elements typically start with uppercase but are still "native" elements
633+ // We consider them HTML-like elements for annotation purposes
634+ if ( REACT_NATIVE_ELEMENTS . includes ( elementName ) ) {
635+ return true ;
636+ }
637+
638+ // Otherwise, assume it's a React component (PascalCase)
639+ return false ;
640+ }
641+
596642function hasAttributeWithName (
597643 openingElement : Babel . NodePath < Babel . types . JSXOpeningElement > ,
598644 name : string | undefined | null
0 commit comments