diff --git a/packages/babel-plugin-component-annotate/README.md b/packages/babel-plugin-component-annotate/README.md index 7d079934..7783c758 100644 --- a/packages/babel-plugin-component-annotate/README.md +++ b/packages/babel-plugin-component-annotate/README.md @@ -47,6 +47,14 @@ Using pnpm: pnpm add @sentry/babel-plugin-component-annotate --save-dev ``` +## Options + +### `ignoredComponents` + +Type: `string[]` + +A list of strings representing the names of components to ignore. The plugin will not apply `data-sentry` annotations on the DOM element for these components. + ## Example ```js @@ -57,7 +65,8 @@ pnpm add @sentry/babel-plugin-component-annotate --save-dev plugins: [ // Put this plugin before any other plugins you have that transform JSX code - ['@sentry/babel-plugin-component-annotate'] + // The options are set by providing an object as the second element in the array, but not required + ['@sentry/babel-plugin-component-annotate', {ignoredComponents: ['Foo', 'Bar']}] ], } ``` diff --git a/packages/babel-plugin-component-annotate/src/index.ts b/packages/babel-plugin-component-annotate/src/index.ts index 5a3e4e5e..0cd087d9 100644 --- a/packages/babel-plugin-component-annotate/src/index.ts +++ b/packages/babel-plugin-component-annotate/src/index.ts @@ -48,15 +48,13 @@ const nativeSourceFileName = "dataSentrySourceFile"; interface AnnotationOpts { native?: boolean; "annotate-fragments"?: boolean; - ignoreComponents?: IgnoredComponent[]; + ignoredComponents?: string[]; } interface AnnotationPluginPass extends PluginPass { opts: AnnotationOpts; } -type IgnoredComponent = [file: string, component: string, element: string]; - type AnnotationPlugin = PluginObj; // We must export the plugin as default, otherwise the Babel loader will not be able to resolve it when configured using its string identifier @@ -78,7 +76,7 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): path.node.id.name, sourceFileNameFromState(state), attributeNamesFromState(state), - state.opts.ignoreComponents ?? [] + state.opts.ignoredComponents ?? [] ); }, ArrowFunctionExpression(path, state) { @@ -106,7 +104,7 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): parent.id.name, sourceFileNameFromState(state), attributeNamesFromState(state), - state.opts.ignoreComponents ?? [] + state.opts.ignoredComponents ?? [] ); }, ClassDeclaration(path, state) { @@ -120,7 +118,7 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): return; } - const ignoredComponents = state.opts.ignoreComponents ?? []; + const ignoredComponents = state.opts.ignoredComponents ?? []; render.traverse({ ReturnStatement(returnStatement) { @@ -153,7 +151,7 @@ function functionBodyPushAttributes( componentName: string, sourceFileName: string | undefined, attributeNames: string[], - ignoredComponents: IgnoredComponent[] + ignoredComponents: string[] ) { let jsxNode: Babel.NodePath; @@ -249,7 +247,7 @@ function processJSX( componentName: string | null, sourceFileName: string | undefined, attributeNames: string[], - ignoredComponents: IgnoredComponent[] + ignoredComponents: string[] ) { if (!jsxNode) { return; @@ -324,7 +322,7 @@ function applyAttributes( componentName: string | null, sourceFileName: string | undefined, attributeNames: string[], - ignoredComponents: IgnoredComponent[] + ignoredComponents: string[] ) { const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames; @@ -340,10 +338,7 @@ function applyAttributes( const elementName = getPathName(t, openingElement); const isAnIgnoredComponent = ignoredComponents.some( - (ignoredComponent) => - matchesIgnoreRule(ignoredComponent[0], sourceFileName) && - matchesIgnoreRule(ignoredComponent[1], componentName) && - matchesIgnoreRule(ignoredComponent[2], elementName) + (ignoredComponent) => ignoredComponent === componentName || ignoredComponent === elementName ); // Add a stable attribute for the element name but only for non-DOM names @@ -501,10 +496,6 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath): return false; } -function matchesIgnoreRule(rule: string, name: string | undefined | null) { - return rule === "*" || rule === name; -} - function hasAttributeWithName( openingElement: Babel.NodePath, name: string | undefined | null diff --git a/packages/babel-plugin-component-annotate/test/test-plugin.test.ts b/packages/babel-plugin-component-annotate/test/test-plugin.test.ts index edb581e0..7dce87ad 100644 --- a/packages/babel-plugin-component-annotate/test/test-plugin.test.ts +++ b/packages/babel-plugin-component-annotate/test/test-plugin.test.ts @@ -1051,856 +1051,15 @@ export default PureComponentName; expect(result?.code).toMatchSnapshot(); }); -it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=nomatch dataSentryElement=nomatch snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -it("ignore components dataSentrySourceFile=* dataSentryComponent=nomatch dataSentryElement=nomatch snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["*", "nomatch", "nomatch"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=* dataSentryElement=nomatch snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "*", "nomatch"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=nomatch dataSentryElement=* snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "*"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=* dataSentryElement=nomatch snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=nomatch dataSentryElement=* snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=* dataSentryElement=* snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - }" - `); -}); - -// This tests out matching only `dataSentryElement`, with * for the others -it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=* dataSentryElement=match snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["*", "*", "Image"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - }" - `); -}); - -// This tests out matching only `dataSentryElement` and `dataSentryComponent`, with * for `dataSentrySourceFile` -it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["*", "Bananas", "Image"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - }" - `); -}); - -// This tests out matching on all 3 of our ignore list values -it("Bananas ignore components dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [plugin, { native: true, ignoreComponents: [["filename-test.js", "Bananas", "Image"]] }], - ], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - }" - `); -}); - -// This tests out matching on all 3 of our ignore list values via * -it("Bananas/Pizza/App ignore components dataSentrySourceFile=* dataSentryComponent=* dataSentryElement=* snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["*", "*", "*"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - } - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - } - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - } - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, null), /*#__PURE__*/React.createElement(PizzaTranslator, null)); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -// This tests out matching on all 3 of our ignore list values -it("Bananas/Pizza/App ignore components dataSentrySourceFile=nomatch dataSentryComponent=* dataSentryElement=* snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "*", "*"]] }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - }, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text, - dataSentryElement: \\"TextInput\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"App\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, { - dataSentryElement: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(PizzaTranslator, { - dataSentryElement: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - })); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -it("Bananas/Pizza/App only Bananas dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [ - plugin, - { - native: true, - ignoreComponents: [ - // Pizza - ["filename-test.js", "PizzaTranslator", "View"], - // App - ["filename-test.js", "App", "View"], - ], - }, - ], - ], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - } - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text, - dataSentryElement: \\"TextInput\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, { - dataSentryElement: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(PizzaTranslator, { - dataSentryElement: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - })); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -it("Bananas/Pizza/App only Pizza dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [ - plugin, - { - native: true, - ignoreComponents: [ - // Bananas - ["filename-test.js", "Bananas", "Image"], - // App - ["filename-test.js", "App", "View"], - ], - }, - ], - ], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - }, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text, - dataSentryElement: \\"TextInput\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, { - dataSentryElement: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(PizzaTranslator, { - dataSentryElement: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - })); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -it("Bananas/Pizza/App only App dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [ - plugin, - { - native: true, - ignoreComponents: [ - // Bananas - ["filename-test.js", "Bananas", "Image"], - // Pizza - ["filename-test.js", "PizzaTranslator", "View"], - ], - }, - ], - ], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - } - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text, - dataSentryElement: \\"TextInput\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"App\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, { - dataSentryElement: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(PizzaTranslator, { - dataSentryElement: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - })); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -it("Bananas/Pizza/App No Pizza Elements dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, +it("Bananas incompatible plugin @react-navigation source snapshot matches", () => { + const result = transform(BananasStandardInput, { + filename: "test/node_modules/@react-navigation/core/filename-test.js", presets: ["@babel/preset-react"], - plugins: [ - [ - plugin, - { - native: true, - ignoreComponents: [ - // Pizza Element - ["filename-test.js", null, "PizzaTranslator"], - ], - }, - ], - ], + plugins: [[plugin, { native: true }]], }); expect(result?.code).toMatchInlineSnapshot(` "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; + import { Image } from 'react-native'; class Bananas extends Component { render() { let pic = { @@ -1913,101 +1072,19 @@ it("Bananas/Pizza/App No Pizza Elements dataSentrySourceFile=match dataSentryCom height: 110, marginTop: 10 }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" + fsClass: \\"test-class\\" }); } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - }, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text, - dataSentryElement: \\"TextInput\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"App\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, { - dataSentryElement: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(PizzaTranslator, null)); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" + }" `); }); -it("Bananas/Pizza/App No Bananas Elements dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { +it("skips components marked in ignoredComponents", () => { const result = transform(BananasPizzaAppStandardInput, { filename: "/filename-test.js", - configFile: false, presets: ["@babel/preset-react"], - plugins: [ - [ - plugin, - { - native: true, - ignoreComponents: [ - // Bananas Element - ["filename-test.js", null, "Bananas"], - ], - }, - ], - ], + plugins: [[plugin, { native: true, ignoredComponents: ["Bananas"] }]], }); - expect(result?.code).toMatchInlineSnapshot(` "import React, { Component } from 'react'; import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; @@ -2024,10 +1101,7 @@ it("Bananas/Pizza/App No Bananas Elements dataSentrySourceFile=match dataSentryC height: 110, marginTop: 10 }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" + fsClass: \\"test-class\\" }); } } @@ -2100,143 +1174,6 @@ it("Bananas/Pizza/App No Bananas Elements dataSentrySourceFile=match dataSentryC `); }); -it("Bananas/Pizza/App No Bananas/Pizza Elements dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [ - plugin, - { - native: true, - ignoreComponents: [ - // Bananas Element - ["filename-test.js", null, "Bananas"], - // Pizza Element - ["filename-test.js", null, "PizzaTranslator"], - ], - }, - ], - ], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; - UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\"; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\", - dataSentryElement: \\"Image\\", - dataSentryComponent: \\"Bananas\\", - dataSentrySourceFile: \\"filename-test.js\\" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - }, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"PizzaTranslator\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(TextInput, { - style: { - backgroundColor: '#000', - color: '#eee', - padding: 8 - }, - placeholder: \\"Type here to translate!\\" // not supported on iOS - , - onChangeText: text => this.setState({ - text - }), - value: this.state.text, - dataSentryElement: \\"TextInput\\", - dataSentrySourceFile: \\"filename-test.js\\" - }), /*#__PURE__*/React.createElement(Text, { - style: { - padding: 10, - fontSize: 42 - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, this.state.text.split(' ').map(word => word && '🍕').join(' '))); - } - } - export default function App() { - return /*#__PURE__*/React.createElement(View, { - style: styles.container, - dataSentryElement: \\"View\\", - dataSentryComponent: \\"App\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, /*#__PURE__*/React.createElement(Text, { - style: { - color: '#eee' - }, - dataSentryElement: \\"Text\\", - dataSentrySourceFile: \\"filename-test.js\\" - }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, null), /*#__PURE__*/React.createElement(PizzaTranslator, null)); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -it("Bananas incompatible plugin @react-navigation source snapshot matches", () => { - const result = transform(BananasStandardInput, { - filename: "test/node_modules/@react-navigation/core/filename-test.js", - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true }]], - }); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - import { Image } from 'react-native'; - class Bananas extends Component { - render() { - let pic = { - uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' - }; - return /*#__PURE__*/React.createElement(Image, { - source: pic, - style: { - width: 193, - height: 110, - marginTop: 10 - }, - fsClass: \\"test-class\\" - }); - } - }" - `); -}); - it("handles ternary operation returned by function body", () => { const result = transform( `const maybeTrue = Math.random() > 0.5; diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index 1a0dd7c1..dc7006dc 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -30,7 +30,7 @@ import { closeSession, DEFAULT_ENVIRONMENT, makeSession } from "@sentry/core"; interface SentryUnpluginFactoryOptions { releaseInjectionPlugin: (injectionCode: string) => UnpluginOptions; - componentNameAnnotatePlugin?: () => UnpluginOptions; + componentNameAnnotatePlugin?: (ignoredComponents?: string[]) => UnpluginOptions; moduleMetadataInjectionPlugin: (injectionCode: string) => UnpluginOptions; debugIdInjectionPlugin: (logger: Logger) => UnpluginOptions; debugIdUploadPlugin: (upload: (buildArtifacts: string[]) => Promise) => UnpluginOptions; @@ -423,7 +423,10 @@ export function sentryUnpluginFactory({ "The component name annotate plugin is currently not supported by '@sentry/esbuild-plugin'" ); } else { - componentNameAnnotatePlugin && plugins.push(componentNameAnnotatePlugin()); + componentNameAnnotatePlugin && + plugins.push( + componentNameAnnotatePlugin(options.reactComponentAnnotation.ignoredComponents) + ); } } @@ -645,7 +648,7 @@ export function createRollupDebugIdUploadHooks( }; } -export function createComponentNameAnnotateHooks() { +export function createComponentNameAnnotateHooks(ignoredComponents?: string[]) { type ParserPlugins = NonNullable< NonNullable[1]>["parserOpts"] >["plugins"]; @@ -673,7 +676,7 @@ export function createComponentNameAnnotateHooks() { try { const result = await transformAsync(code, { - plugins: [[componentNameAnnotatePlugin]], + plugins: [[componentNameAnnotatePlugin, { ignoredComponents }]], filename: id, parserOpts: { sourceType: "module", diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index c42e5b10..b6b5052e 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -319,6 +319,10 @@ export interface Options { * Whether the component name annotate plugin should be enabled or not. */ enabled?: boolean; + /** + * A list of strings representing the names of components to ignore. The plugin will not apply `data-sentry` annotations on the DOM element for these components. + */ + ignoredComponents?: string[]; }; /** diff --git a/packages/dev-utils/src/generate-documentation-table.ts b/packages/dev-utils/src/generate-documentation-table.ts index d13c8ac3..bd56e487 100644 --- a/packages/dev-utils/src/generate-documentation-table.ts +++ b/packages/dev-utils/src/generate-documentation-table.ts @@ -390,6 +390,13 @@ type IncludeEntry = { fullDescription: "Whether the component name annotate plugin should be enabled or not.", supportedBundlers: ["webpack", "vite", "rollup"], }, + { + name: "ignoredComponents", + type: "string[]", + fullDescription: + "A list of strings representing the names of components to ignore. The plugin will not perform apply `data-sentry` annotations on the DOM element for these components.", + supportedBundlers: ["webpack", "vite", "rollup"], + }, ], }, { diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index dbdd63b8..de29484f 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -18,10 +18,10 @@ function rollupReleaseInjectionPlugin(injectionCode: string): UnpluginOptions { }; } -function rollupComponentNameAnnotatePlugin(): UnpluginOptions { +function rollupComponentNameAnnotatePlugin(ignoredComponents?: string[]): UnpluginOptions { return { name: "sentry-rollup-component-name-annotate-plugin", - rollup: createComponentNameAnnotateHooks(), + rollup: createComponentNameAnnotateHooks(ignoredComponents), }; } diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 9aa1ee34..11c77f00 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -21,11 +21,11 @@ function viteReleaseInjectionPlugin(injectionCode: string): UnpluginOptions { }; } -function viteComponentNameAnnotatePlugin(): UnpluginOptions { +function viteComponentNameAnnotatePlugin(ignoredComponents?: string[]): UnpluginOptions { return { name: "sentry-vite-component-name-annotate-plugin", enforce: "pre" as const, - vite: createComponentNameAnnotateHooks(), + vite: createComponentNameAnnotateHooks(ignoredComponents), }; } diff --git a/packages/webpack-plugin/src/index.ts b/packages/webpack-plugin/src/index.ts index 66790c34..f215630c 100644 --- a/packages/webpack-plugin/src/index.ts +++ b/packages/webpack-plugin/src/index.ts @@ -47,7 +47,7 @@ function webpackReleaseInjectionPlugin(injectionCode: string): UnpluginOptions { }; } -function webpackComponentNameAnnotatePlugin(): UnpluginOptions { +function webpackComponentNameAnnotatePlugin(ignoredComponents?: string[]): UnpluginOptions { return { name: "sentry-webpack-component-name-annotate-plugin", enforce: "pre", @@ -55,7 +55,7 @@ function webpackComponentNameAnnotatePlugin(): UnpluginOptions { transformInclude(id) { return id.endsWith(".tsx") || id.endsWith(".jsx"); }, - transform: createComponentNameAnnotateHooks().transform, + transform: createComponentNameAnnotateHooks(ignoredComponents).transform, }; }