Skip to content

Commit 148b620

Browse files
committed
fix: Inject component annotations into HTML elements
1 parent cd26b63 commit 148b620

File tree

4 files changed

+313
-89
lines changed

4 files changed

+313
-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: 62 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,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
*/
313330
function 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+
596648
function hasAttributeWithName(
597649
openingElement: Babel.NodePath<Babel.types.JSXOpeningElement>,
598650
name: string | undefined | null

0 commit comments

Comments
 (0)