Skip to content

Commit 32c96e7

Browse files
committed
fix: Inject component annotations into HTML elements
1 parent cd26b63 commit 32c96e7

File tree

4 files changed

+305
-89
lines changed

4 files changed

+305
-89
lines changed

packages/babel-plugin-component-annotate/src/constants.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,26 @@ export const KNOWN_INCOMPATIBLE_PLUGINS = [
3030
"@react-navigation",
3131
];
3232

33-
export const DEFAULT_IGNORED_ELEMENTS = [
33+
export const REACT_NATIVE_ELEMENTS = [
34+
"View",
35+
"Text",
36+
"Image",
37+
"ScrollView",
38+
"TextInput",
39+
"TouchableOpacity",
40+
"TouchableHighlight",
41+
"TouchableWithoutFeedback",
42+
"FlatList",
43+
"SectionList",
44+
"Button",
45+
"ActivityIndicator",
46+
"Modal",
47+
"Switch",
48+
"Picker",
49+
"Slider",
50+
];
51+
52+
export const DEFAULT_HTML_ELEMENTS = [
3453
"a",
3554
"abbr",
3655
"address",

packages/babel-plugin-component-annotate/src/index.ts

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,17 @@
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

3536
import type * as Babel from "@babel/core";
3637
import 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

4045
const webComponentName = "data-sentry-component";
4146
const webElementName = "data-sentry-element";
@@ -251,6 +256,7 @@ function functionBodyPushAttributes(
251256
function 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
*/
313328
function 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+
596646
function hasAttributeWithName(
597647
openingElement: Babel.NodePath<Babel.types.JSXOpeningElement>,
598648
name: string | undefined | null

0 commit comments

Comments
 (0)