Skip to content

Commit 729d328

Browse files
committed
fix: Inject component annotations into HTML elements
1 parent cd26b63 commit 729d328

File tree

4 files changed

+154
-90
lines changed

4 files changed

+154
-90
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: 56 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,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
*/
313324
function 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+
596642
function hasAttributeWithName(
597643
openingElement: Babel.NodePath<Babel.types.JSXOpeningElement>,
598644
name: string | undefined | null

0 commit comments

Comments
 (0)