diff --git a/.gitignore b/.gitignore index 1126d8eb..a237aad7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ yarn-error.log packages/**/yarn.lock .DS_Store -packages/bundler-plugins/src/core/version.ts packages/integration-tests-next/fixtures/**/pnpm-lock.yaml diff --git a/package.json b/package.json index 8d03e524..d9bac504 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "workspaces": [ "packages/babel-plugin-component-annotate", "packages/bundler-plugin-core", - "packages/bundler-plugins", "packages/dev-utils", "packages/esbuild-plugin", "packages/playground", diff --git a/packages/babel-plugin-component-annotate/package.json b/packages/babel-plugin-component-annotate/package.json index 6506229f..fe868f0e 100644 --- a/packages/babel-plugin-component-annotate/package.json +++ b/packages/babel-plugin-component-annotate/package.json @@ -48,7 +48,7 @@ "clean:deps": "premove node_modules" }, "dependencies": { - "@sentry/bundler-plugins": "5.3.0" + "@sentry/bundler-plugins": "^10.62.0" }, "devDependencies": { "@types/node": "^18.6.3", diff --git a/packages/bundler-plugin-core/package.json b/packages/bundler-plugin-core/package.json index a71fa7d9..17d41fd7 100644 --- a/packages/bundler-plugin-core/package.json +++ b/packages/bundler-plugin-core/package.json @@ -49,7 +49,7 @@ "clean:deps": "premove node_modules" }, "dependencies": { - "@sentry/bundler-plugins": "5.3.0" + "@sentry/bundler-plugins": "^10.62.0" }, "devDependencies": { "@types/node": "^18.6.3", diff --git a/packages/bundler-plugins/.gitignore b/packages/bundler-plugins/.gitignore deleted file mode 100644 index 1521c8b7..00000000 --- a/packages/bundler-plugins/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json deleted file mode 100644 index 44ad4d31..00000000 --- a/packages/bundler-plugins/package.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "name": "@sentry/bundler-plugins", - "version": "5.3.0", - "description": "Sentry Bundler Plugins", - "repository": "git://github.com/getsentry/sentry-javascript-bundler-plugins.git", - "homepage": "https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/bundler-plugins", - "author": "Sentry", - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "files": [ - "dist", - "sentry-release-injection-file.js", - "sentry-esbuild-debugid-injection-file.js" - ], - "exports": { - "./webpack": { - "types": "./dist/types/webpack/index.d.ts", - "import": "./dist/esm/webpack/index.mjs", - "require": "./dist/cjs/webpack/index.js" - }, - "./webpack5": { - "types": "./dist/types/webpack/webpack5.d.ts", - "import": "./dist/esm/webpack/webpack5.mjs", - "require": "./dist/cjs/webpack/webpack5.js" - }, - "./rollup": { - "types": "./dist/types/rollup/index.d.ts", - "import": "./dist/esm/rollup/index.mjs", - "require": "./dist/cjs/rollup/index.js" - }, - "./vite": { - "types": "./dist/types/vite/index.d.ts", - "import": "./dist/esm/vite/index.mjs", - "require": "./dist/cjs/vite/index.js" - }, - "./esbuild": { - "types": "./dist/types/esbuild/index.d.ts", - "import": "./dist/esm/esbuild/index.mjs", - "require": "./dist/cjs/esbuild/index.js" - }, - "./core": { - "types": "./dist/types/core/index.d.ts", - "import": "./dist/esm/core/index.mjs", - "require": "./dist/cjs/core/index.js" - }, - "./babel-plugin": { - "types": "./dist/types/babel-plugin/index.d.ts", - "import": "./dist/esm/babel-plugin/index.mjs", - "require": "./dist/cjs/babel-plugin/index.js" - }, - "./sentry-release-injection-file": { - "import": "./sentry-release-injection-file.js", - "require": "./sentry-release-injection-file.js" - }, - "./sentry-esbuild-debugid-injection-file": { - "import": "./sentry-esbuild-debugid-injection-file.js", - "require": "./sentry-esbuild-debugid-injection-file.js" - }, - "./webpack-loader": { - "require": "./dist/cjs/webpack/component-annotation-transform.js" - } - }, - "scripts": { - "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/core/version.ts", - "build": "premove ./dist && run-p build:rollup build:types && run-s build:npm", - "build:watch": "run-p build:rollup:watch build:types:watch", - "build:rollup": "rolldown --config rollup.config.mjs", - "build:rollup:watch": "rolldown --config rollup.config.mjs --watch --no-watch.clearScreen", - "build:types": "tsc --project types.tsconfig.json", - "build:types:watch": "tsc --project types.tsconfig.json --watch --preserveWatchOutput", - "build:npm": "npm pack", - "check:types": "run-p check:types:src check:types:test", - "check:types:src": "tsc --project ./tsconfig.json --noEmit", - "check:types:test": "tsc --project ./test/tsconfig.json --noEmit", - "clean": "run-s clean:build", - "clean:all": "run-p clean clean:deps", - "clean:build": "premove ./dist *.tgz", - "clean:deps": "premove node_modules", - "pretest": "yarn prebuild", - "test": "vitest run" - }, - "dependencies": { - "@babel/core": "^7.18.5", - "@sentry/cli": "^2.58.6", - "dotenv": "^16.3.1", - "find-up": "^5.0.0", - "glob": "^13.0.6", - "magic-string": "~0.30.8" - }, - "peerDependencies": { - "rollup": ">=3.2.0", - "webpack": ">=5.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "webpack": { - "optional": true - } - }, - "devDependencies": { - "@babel/preset-react": "^7.23.3", - "@sentry/core": "10.56.0", - "@sentry/types": "10.56.0", - "@sentry-internal/dev-utils": "5.3.0", - "@types/babel__core": "^7.20.5", - "@types/node": "^18.6.3", - "@types/webpack": "npm:@types/webpack@^4", - "premove": "^4.0.0", - "rolldown": "^1.0.0", - "vitest": "^4.0.0", - "webpack": "5.76.0" - }, - "volta": { - "extends": "../../package.json" - }, - "engines": { - "node": ">= 18" - }, - "sideEffects": [ - "./sentry-release-injection-file.js", - "./sentry-esbuild-debugid-injection-file.js" - ] -} diff --git a/packages/bundler-plugins/rollup.config.mjs b/packages/bundler-plugins/rollup.config.mjs deleted file mode 100644 index 8068953e..00000000 --- a/packages/bundler-plugins/rollup.config.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import packageJson from "./package.json" with { type: "json" }; -import modulePackage from "module"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const srcDir = path.resolve(__dirname, "src"); - -const external = [ - ...Object.keys(packageJson.dependencies || {}), - ...modulePackage.builtinModules, - "webpack", - "rollup", - "vite", -]; - -export default { - platform: "node", - input: [ - "src/babel-plugin/index.ts", - "src/core/index.ts", - "src/rollup/index.ts", - "src/vite/index.ts", - "src/esbuild/index.ts", - "src/webpack/index.ts", - "src/webpack/webpack5.ts", - "src/webpack/component-annotation-transform.ts", - ], - external, - output: [ - { - dir: "./dist/esm", - format: "esm", - exports: "named", - sourcemap: true, - entryFileNames: (chunkInfo) => { - if (chunkInfo.facadeModuleId) { - const rel = path.relative(srcDir, chunkInfo.facadeModuleId); - return rel.replace(/\.ts$/, ".mjs"); - } - return "[name].mjs"; - }, - chunkFileNames: "_chunks/[name]-[hash].mjs", - }, - { - dir: "./dist/cjs", - format: "cjs", - exports: "named", - sourcemap: true, - entryFileNames: (chunkInfo) => { - if (chunkInfo.facadeModuleId) { - const rel = path.relative(srcDir, chunkInfo.facadeModuleId); - return rel.replace(/\.ts$/, ".js"); - } - return "[name].js"; - }, - chunkFileNames: "_chunks/[name]-[hash].js", - }, - ], -}; diff --git a/packages/bundler-plugins/sentry-esbuild-debugid-injection-file.js b/packages/bundler-plugins/sentry-esbuild-debugid-injection-file.js deleted file mode 100644 index 06ad5071..00000000 --- a/packages/bundler-plugins/sentry-esbuild-debugid-injection-file.js +++ /dev/null @@ -1,20 +0,0 @@ -try { - let globalObject = - "undefined" != typeof window - ? window - : "undefined" != typeof global - ? global - : "undefined" != typeof globalThis - ? global - : "undefined" != typeof self - ? self - : {}; - - let stack = new globalObject.Error().stack; - - if (stack) { - globalObject._sentryDebugIds = globalObject._sentryDebugIds || {}; - globalObject._sentryDebugIds[stack] = "__SENTRY_DEBUG_ID__"; - globalObject._sentryDebugIdIdentifier = "sentry-dbid-__SENTRY_DEBUG_ID__"; - } -} catch {} diff --git a/packages/bundler-plugins/sentry-release-injection-file.js b/packages/bundler-plugins/sentry-release-injection-file.js deleted file mode 100644 index 51de6958..00000000 --- a/packages/bundler-plugins/sentry-release-injection-file.js +++ /dev/null @@ -1,4 +0,0 @@ -// This const is used for nothing except to make this file identifiable via its content. -// We search for "_sentry_release_injection_file" in the plugin to determine for sure that the file we look at is the release injection file. - -// _sentry_release_injection_file diff --git a/packages/bundler-plugins/src/babel-plugin/constants.ts b/packages/bundler-plugins/src/babel-plugin/constants.ts deleted file mode 100644 index 51eac57d..00000000 --- a/packages/bundler-plugins/src/babel-plugin/constants.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2020 Engineering at FullStory - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -export const KNOWN_INCOMPATIBLE_PLUGINS = [ - // This module might be causing an issue preventing clicks. For safety, we won't run on this module. - "react-native-testfairy", - // This module checks for unexpected property keys and throws an exception. - "@react-navigation", -]; - -export const DEFAULT_IGNORED_ELEMENTS = [ - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hgroup", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "main", - "map", - "mark", - "menu", - "menuitem", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "pre", - "progress", - "q", - "rb", - "rp", - "rt", - "rtc", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "template", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", -]; diff --git a/packages/bundler-plugins/src/babel-plugin/experimental.ts b/packages/bundler-plugins/src/babel-plugin/experimental.ts deleted file mode 100644 index 130f4abc..00000000 --- a/packages/bundler-plugins/src/babel-plugin/experimental.ts +++ /dev/null @@ -1,603 +0,0 @@ -/* oxlint-disable max-lines */ -/** - * MIT License - * - * Copyright (c) 2020 Engineering at FullStory - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -/** - * The following code is based on the FullStory Babel plugin, but has been modified to work - * with Sentry products: - * - * - Added `sentry` to data properties, i.e `data-sentry-component` - * - Converted to TypeScript - * - Code cleanups - * - Highly modified to inject the data attributes into the root HTML elements of a component. - */ - -import type * as Babel from "@babel/core"; -import type { PluginObj, PluginPass } from "@babel/core"; - -const REACT_NATIVE_ELEMENTS: string[] = [ - "Image", - "Text", - "View", - "ScrollView", - "TextInput", - "TouchableOpacity", - "TouchableHighlight", - "TouchableWithoutFeedback", - "FlatList", - "SectionList", - "ActivityIndicator", - "Button", - "Switch", - "Modal", - "SafeAreaView", - "StatusBar", - "KeyboardAvoidingView", - "RefreshControl", - "Picker", - "Slider", -]; - -interface AnnotationOpts { - native?: boolean; - ignoredComponents?: string[]; -} - -interface FragmentContext { - fragmentAliases: Set; - reactNamespaceAliases: Set; -} - -interface AnnotationPluginPass extends PluginPass { - opts: AnnotationOpts; - sentryFragmentContext?: FragmentContext; -} - -type AnnotationPlugin = PluginObj; - -// Shared context object for all JSX processing functions -interface JSXProcessingContext { - /** Babel types object */ - t: typeof Babel.types; - /** Name of the React component */ - componentName: string; - /** AAttribute name for the component */ - attributeName: string; - /** Array of component names to ignore */ - ignoredComponents: string[]; - /** Fragment context for identifying React fragments */ - fragmentContext?: FragmentContext; -} - -// We must export the plugin as default, otherwise the Babel loader will not be able to resolve it when configured using its string identifier -export function experimentalComponentNameAnnotatePlugin({ - types: t, -}: typeof Babel): AnnotationPlugin { - return { - visitor: { - Program: { - enter(path, state) { - const fragmentContext = collectFragmentContext(path); - state.sentryFragmentContext = fragmentContext; - }, - }, - FunctionDeclaration(path, state) { - if (!path.node.id?.name) { - return; - } - - const context = createJSXProcessingContext(state, t, path.node.id.name); - functionBodyPushAttributes(context, path); - }, - ArrowFunctionExpression(path, state) { - // We're expecting a `VariableDeclarator` like `const MyComponent =` - const parent = path.parent; - - if ( - !parent || - !("id" in parent) || - !parent.id || - !("name" in parent.id) || - !parent.id.name - ) { - return; - } - - const context = createJSXProcessingContext(state, t, parent.id.name); - functionBodyPushAttributes(context, path); - }, - ClassDeclaration(path, state) { - const name = path.get("id"); - const properties = path.get("body").get("body"); - const render = properties.find((prop) => { - return prop.isClassMethod() && prop.get("key").isIdentifier({ name: "render" }); - }); - - if (!render?.traverse) { - return; - } - - const context = createJSXProcessingContext(state, t, name.node?.name || ""); - - render.traverse({ - ReturnStatement(returnStatement) { - const arg = returnStatement.get("argument"); - - if (!arg.isJSXElement() && !arg.isJSXFragment()) { - return; - } - - processJSX(context, arg); - }, - }); - }, - }, - }; -} - -/** - * Checks if an element name represents an HTML element (as opposed to a React component). - * HTML elements include standard lowercase HTML tags and React Native elements. - */ -function isHtmlElement(elementName: string): boolean { - // Unknown elements are not HTML elements - if (elementName === UNKNOWN_ELEMENT_NAME) { - return false; - } - - // Check for lowercase first letter (standard HTML elements) - if (elementName.length > 0 && elementName.charAt(0) === elementName.charAt(0).toLowerCase()) { - return true; - } - - // React Native elements typically start with uppercase but are still "native" elements - // We consider them HTML-like elements for annotation purposes - if (REACT_NATIVE_ELEMENTS.includes(elementName)) { - return true; - } - - // Otherwise, assume it's a React component (PascalCase) - return false; -} - -/** - * Creates a JSX processing context from the plugin state - */ -function createJSXProcessingContext( - state: AnnotationPluginPass, - t: typeof Babel.types, - componentName: string -): JSXProcessingContext { - return { - t, - componentName, - attributeName: attributeNamesFromState(state), - ignoredComponents: state.opts.ignoredComponents ?? [], - fragmentContext: state.sentryFragmentContext, - }; -} - -/** - * Processes the body of a function to add Sentry tracking attributes to JSX elements. - * Handles various function body structures including direct JSX returns, conditional expressions, - * and nested JSX elements. - */ -function functionBodyPushAttributes( - context: JSXProcessingContext, - path: Babel.NodePath -): void { - let jsxNode: Babel.NodePath; - - const functionBody = path.get("body").get("body"); - - if ( - !("length" in functionBody) && - functionBody.parent && - (functionBody.parent.type === "JSXElement" || functionBody.parent.type === "JSXFragment") - ) { - const maybeJsxNode = functionBody.find((c) => { - return c.type === "JSXElement" || c.type === "JSXFragment"; - }); - - if (!maybeJsxNode) { - return; - } - - jsxNode = maybeJsxNode; - } else { - const returnStatement = functionBody.find((c) => { - return c.type === "ReturnStatement"; - }); - if (!returnStatement) { - return; - } - - const arg = returnStatement.get("argument"); - if (!arg) { - return; - } - - if (Array.isArray(arg)) { - return; - } - - // Handle the case of a function body returning a ternary operation. - // `return (maybeTrue ? '' : ())` - if (arg.isConditionalExpression()) { - const consequent = arg.get("consequent"); - if (consequent.isJSXFragment() || consequent.isJSXElement()) { - processJSX(context, consequent); - } - const alternate = arg.get("alternate"); - if (alternate.isJSXFragment() || alternate.isJSXElement()) { - processJSX(context, alternate); - } - return; - } - - if (!arg.isJSXFragment() && !arg.isJSXElement()) { - return; - } - - jsxNode = arg; - } - - if (!jsxNode) { - return; - } - - processJSX(context, jsxNode); -} - -/** - * Recursively processes JSX elements to add Sentry tracking attributes. - * Handles both JSX elements and fragments, applying appropriate attributes - * based on configuration and component context. - */ -function processJSX(context: JSXProcessingContext, jsxNode: Babel.NodePath): void { - if (!jsxNode) { - return; - } - - // NOTE: I don't know of a case where `openingElement` would have more than one item, - // but it's safer to always iterate - const paths = jsxNode.get("openingElement"); - const openingElements = ( - Array.isArray(paths) ? paths : [paths] - ) as Babel.NodePath[]; - - const hasInjectedAttributes = openingElements.reduce( - (prev, openingElement) => - prev || applyAttributes(context, openingElement, context.componentName), - false - ); - - if (hasInjectedAttributes) { - return; - } - - let children = jsxNode.get("children"); - // TODO: See why `Array.isArray` doesn't have correct behaviour here - if (children && !("length" in children)) { - // A single child was found, maybe a bit of static text - children = [children]; - } - - children.forEach((child) => { - // Happens for some node types like plain text - if (!child.node) { - return; - } - - // If the current element is a fragment, children are still considered at root level - // Otherwise, children are not at root level - const openingElement = child.get("openingElement"); - // TODO: Improve this. We never expect to have multiple opening elements - // but if it's possible, this should work - if (Array.isArray(openingElement)) { - return; - } - - processJSX(context, child); - }); -} - -/** - * Applies Sentry tracking attributes to a JSX opening element. - * Adds component name, element name, and source file attributes while - * respecting ignore lists and fragment detection. - */ -function applyAttributes( - context: JSXProcessingContext, - openingElement: Babel.NodePath, - componentName: string -): boolean { - const { t, attributeName: componentAttributeName, ignoredComponents, fragmentContext } = context; - - // e.g., Raw JSX text like the `A` in `

a

` - if (!openingElement.node) { - return false; - } - - // Check if this is a React fragment - if so, skip attribute addition entirely - const isFragment = isReactFragment(t, openingElement, fragmentContext); - if (isFragment) { - return false; - } - - if (!openingElement.node.attributes) { - openingElement.node.attributes = []; - } - - const elementName = getPathName(t, openingElement); - - if (!isHtmlElement(elementName)) { - return false; - } - - const isAnIgnoredComponent = ignoredComponents.some( - (ignoredComponent) => ignoredComponent === componentName || ignoredComponent === elementName - ); - - // Add a stable attribute for the component name (only for root elements) - if (!isAnIgnoredComponent && !hasAttributeWithName(openingElement, componentAttributeName)) { - if (componentAttributeName) { - openingElement.node.attributes.push( - t.jSXAttribute(t.jSXIdentifier(componentAttributeName), t.stringLiteral(componentName)) - ); - } - } - - return true; -} - -function attributeNamesFromState(state: AnnotationPluginPass): string { - if (state.opts.native) { - return "dataSentryComponent"; - } - - return "data-sentry-component"; -} - -function collectFragmentContext(programPath: Babel.NodePath): FragmentContext { - const fragmentAliases = new Set(); - const reactNamespaceAliases = new Set(["React"]); // Default React namespace - - programPath.traverse({ - ImportDeclaration(importPath) { - const source = importPath.node.source.value; - - // Handle React imports - if (source === "react" || source === "React") { - importPath.node.specifiers.forEach((spec) => { - if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") { - // Detect aliased React.Fragment imports (e.g., `Fragment as F`) - // so we can later identify as a fragment in JSX. - if (spec.imported.name === "Fragment") { - fragmentAliases.add(spec.local.name); - } - } else if ( - spec.type === "ImportDefaultSpecifier" || - spec.type === "ImportNamespaceSpecifier" - ) { - // import React from 'react' -> React OR - // import * as React from 'react' -> React - reactNamespaceAliases.add(spec.local.name); - } - }); - } - }, - - // Handle simple variable assignments only (avoid complex cases) - VariableDeclarator(varPath) { - if (varPath.node.init) { - const init = varPath.node.init; - - // Handle identifier assignments: const MyFragment = Fragment - if (varPath.node.id.type === "Identifier") { - // Handle: const MyFragment = Fragment (only if Fragment is a known alias) - if (init.type === "Identifier" && fragmentAliases.has(init.name)) { - fragmentAliases.add(varPath.node.id.name); - } - - // Handle: const MyFragment = React.Fragment (only for known React namespaces) - if ( - init.type === "MemberExpression" && - init.object.type === "Identifier" && - init.property.type === "Identifier" && - init.property.name === "Fragment" && - reactNamespaceAliases.has(init.object.name) - ) { - fragmentAliases.add(varPath.node.id.name); - } - } - - // Handle destructuring assignments: const { Fragment } = React - if (varPath.node.id.type === "ObjectPattern") { - if (init.type === "Identifier" && reactNamespaceAliases.has(init.name)) { - const properties = varPath.node.id.properties; - - for (const prop of properties) { - if ( - prop.type === "ObjectProperty" && - prop.key?.type === "Identifier" && - prop.value?.type === "Identifier" && - prop.key.name === "Fragment" - ) { - fragmentAliases.add(prop.value.name); - } - } - } - } - } - }, - }); - - return { fragmentAliases, reactNamespaceAliases }; -} - -function isReactFragment( - t: typeof Babel.types, - openingElement: Babel.NodePath, - context?: FragmentContext // Add this optional parameter -): boolean { - // Handle JSX fragments (<>) - if (openingElement.isJSXFragment()) { - return true; - } - - const elementName = getPathName(t, openingElement); - - // Direct fragment references - if (elementName === "Fragment" || elementName === "React.Fragment") { - return true; - } - - // TODO: All these objects are typed as unknown, maybe an oversight in Babel types? - - // Check if the element name is a known fragment alias - if (context && elementName && context.fragmentAliases.has(elementName)) { - return true; - } - - // Handle JSXMemberExpression - if ( - openingElement.node && - "name" in openingElement.node && - openingElement.node.name && - typeof openingElement.node.name === "object" && - "type" in openingElement.node.name && - openingElement.node.name.type === "JSXMemberExpression" - ) { - const nodeName = openingElement.node.name; - if (typeof nodeName !== "object" || !nodeName) { - return false; - } - - if ("object" in nodeName && "property" in nodeName) { - const nodeNameObject = nodeName.object; - const nodeNameProperty = nodeName.property; - - if (typeof nodeNameObject !== "object" || typeof nodeNameProperty !== "object") { - return false; - } - - if (!nodeNameObject || !nodeNameProperty) { - return false; - } - - const objectName = "name" in nodeNameObject && nodeNameObject.name; - const propertyName = "name" in nodeNameProperty && nodeNameProperty.name; - - // React.Fragment check - if (objectName === "React" && propertyName === "Fragment") { - return true; - } - - // Enhanced checks using context - if (context) { - // Check React.Fragment pattern with known React namespaces - if ( - context.reactNamespaceAliases.has(objectName as string) && - propertyName === "Fragment" - ) { - return true; - } - - // Check MyFragment.Fragment pattern - if (context.fragmentAliases.has(objectName as string) && propertyName === "Fragment") { - return true; - } - } - } - } - - return false; -} - -function hasAttributeWithName( - openingElement: Babel.NodePath, - name: string | undefined | null -): boolean { - if (!name) { - return false; - } - - return openingElement.node.attributes.some((node) => { - if (node.type === "JSXAttribute") { - return node.name.name === name; - } - - return false; - }); -} - -function getPathName(t: typeof Babel.types, path: Babel.NodePath): string { - if (!path.node) return UNKNOWN_ELEMENT_NAME; - if (!("name" in path.node)) { - return UNKNOWN_ELEMENT_NAME; - } - - const name = path.node.name; - - if (typeof name === "string") { - return name; - } - - if (t.isIdentifier(name) || t.isJSXIdentifier(name)) { - return name.name; - } - - if (t.isJSXNamespacedName(name)) { - return name.name.name; - } - - // Handle JSX member expressions like Tab.Group - if (t.isJSXMemberExpression(name)) { - const objectName = getJSXMemberExpressionObjectName(t, name.object); - const propertyName = name.property.name; - return `${objectName}.${propertyName}`; - } - - return UNKNOWN_ELEMENT_NAME; -} - -// Recursively handle nested member expressions (e.g. Components.UI.Header) -function getJSXMemberExpressionObjectName( - t: typeof Babel.types, - object: Babel.types.JSXMemberExpression | Babel.types.JSXIdentifier -): string { - if (t.isJSXIdentifier(object)) { - return object.name; - } - if (t.isJSXMemberExpression(object)) { - const objectName = getJSXMemberExpressionObjectName(t, object.object); - return `${objectName}.${object.property.name}`; - } - - return UNKNOWN_ELEMENT_NAME; -} - -const UNKNOWN_ELEMENT_NAME = "unknown"; diff --git a/packages/bundler-plugins/src/babel-plugin/index.ts b/packages/bundler-plugins/src/babel-plugin/index.ts deleted file mode 100644 index 13773f43..00000000 --- a/packages/bundler-plugins/src/babel-plugin/index.ts +++ /dev/null @@ -1,890 +0,0 @@ -/* oxlint-disable max-lines */ -/** - * MIT License - * - * Copyright (c) 2020 Engineering at FullStory - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -/** - * The following code is based on the FullStory Babel plugin, but has been modified to work - * with Sentry products: - * - * - Added `sentry` to data properties, i.e `data-sentry-component` - * - Converted to TypeScript - * - Code cleanups - */ - -import type * as Babel from "@babel/core"; -import type { PluginObj, PluginPass } from "@babel/core"; - -import { DEFAULT_IGNORED_ELEMENTS, KNOWN_INCOMPATIBLE_PLUGINS } from "./constants"; - -const webComponentName = "data-sentry-component"; -const webElementName = "data-sentry-element"; -const webSourceFileName = "data-sentry-source-file"; - -const nativeComponentName = "dataSentryComponent"; -const nativeElementName = "dataSentryElement"; -const nativeSourceFileName = "dataSentrySourceFile"; - -const SENTRY_LABEL_ATTRIBUTE = "sentry-label"; -const MAX_LABEL_LENGTH = 64; -const DEFAULT_TEXT_COMPONENT_NAMES = ["Text", "text"]; -const MAX_TEXT_SEARCH_DEPTH = 3; - -interface AutoInjectSentryLabelOpts { - textComponentNames?: string[]; -} - -interface AnnotationOpts { - native?: boolean; - "annotate-fragments"?: boolean; - ignoredComponents?: string[]; - /** @hidden */ - autoInjectSentryLabel?: boolean | AutoInjectSentryLabelOpts; -} - -interface FragmentContext { - fragmentAliases: Set; - reactNamespaceAliases: Set; -} - -interface AnnotationPluginPass extends PluginPass { - opts: AnnotationOpts; - sentryFragmentContext?: FragmentContext; -} - -type AnnotationPlugin = PluginObj; - -// Shared context object for all JSX processing functions -interface JSXProcessingContext { - /** Whether to annotate React fragments */ - annotateFragments: boolean; - /** Babel types object */ - t: typeof Babel.types; - /** Name of the React component */ - componentName: string; - /** Source file name (optional) */ - sourceFileName?: string; - /** Array of attribute names [component, element, sourceFile] */ - attributeNames: string[]; - /** Array of component names to ignore */ - ignoredComponents: string[]; - /** Fragment context for identifying React fragments */ - fragmentContext?: FragmentContext; - /** Whether to auto-inject sentry-label from static text children */ - autoInjectSentryLabel: boolean; - /** Component names whose JSXText children are considered text content */ - textComponentNames: string[]; -} - -export { experimentalComponentNameAnnotatePlugin } from "./experimental"; - -// We must export the plugin as default, otherwise the Babel loader will not be able to resolve it when configured using its string identifier -export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): AnnotationPlugin { - return { - visitor: { - Program: { - enter(path, state) { - const fragmentContext = collectFragmentContext(path); - state.sentryFragmentContext = fragmentContext; - }, - }, - FunctionDeclaration(path, state) { - if (!path.node.id?.name) { - return; - } - if (isKnownIncompatiblePluginFromState(state)) { - return; - } - - const context = createJSXProcessingContext(state, t, path.node.id.name); - functionBodyPushAttributes(context, path); - }, - ArrowFunctionExpression(path, state) { - // We're expecting a `VariableDeclarator` like `const MyComponent =` - const parent = path.parent; - - if ( - !parent || - !("id" in parent) || - !parent.id || - !("name" in parent.id) || - !parent.id.name - ) { - return; - } - - if (isKnownIncompatiblePluginFromState(state)) { - return; - } - - const context = createJSXProcessingContext(state, t, parent.id.name); - functionBodyPushAttributes(context, path); - }, - ClassDeclaration(path, state) { - const name = path.get("id"); - const properties = path.get("body").get("body"); - const render = properties.find((prop) => { - return prop.isClassMethod() && prop.get("key").isIdentifier({ name: "render" }); - }); - - if (!render?.traverse || isKnownIncompatiblePluginFromState(state)) { - return; - } - - const context = createJSXProcessingContext(state, t, name.node?.name || ""); - - render.traverse({ - ReturnStatement(returnStatement) { - const arg = returnStatement.get("argument"); - - if (!arg.isJSXElement() && !arg.isJSXFragment()) { - return; - } - - processJSX(context, arg); - }, - }); - }, - }, - }; -} - -/** - * Creates a JSX processing context from the plugin state - */ -function createJSXProcessingContext( - state: AnnotationPluginPass, - t: typeof Babel.types, - componentName: string -): JSXProcessingContext { - return { - annotateFragments: state.opts["annotate-fragments"] === true, - t, - componentName, - sourceFileName: sourceFileNameFromState(state), - attributeNames: attributeNamesFromState(state), - ignoredComponents: state.opts.ignoredComponents ?? [], - fragmentContext: state.sentryFragmentContext, - autoInjectSentryLabel: !!state.opts.autoInjectSentryLabel, - textComponentNames: - (state.opts.autoInjectSentryLabel && typeof state.opts.autoInjectSentryLabel === "object" - ? state.opts.autoInjectSentryLabel.textComponentNames - : undefined) ?? DEFAULT_TEXT_COMPONENT_NAMES, - }; -} - -/** - * Processes the body of a function to add Sentry tracking attributes to JSX elements. - * Handles various function body structures including direct JSX returns, conditional expressions, - * and nested JSX elements. - */ -function functionBodyPushAttributes( - context: JSXProcessingContext, - path: Babel.NodePath -): void { - let jsxNode: Babel.NodePath; - - const functionBody = path.get("body").get("body"); - - if ( - !("length" in functionBody) && - functionBody.parent && - (functionBody.parent.type === "JSXElement" || functionBody.parent.type === "JSXFragment") - ) { - const maybeJsxNode = functionBody.find((c) => { - return c.type === "JSXElement" || c.type === "JSXFragment"; - }); - - if (!maybeJsxNode) { - return; - } - - jsxNode = maybeJsxNode; - } else { - const returnStatement = functionBody.find((c) => { - return c.type === "ReturnStatement"; - }); - if (!returnStatement) { - return; - } - - const arg = returnStatement.get("argument"); - if (!arg) { - return; - } - - if (Array.isArray(arg)) { - return; - } - - // Handle the case of a function body returning a ternary operation. - // `return (maybeTrue ? '' : ())` - if (arg.isConditionalExpression()) { - const consequent = arg.get("consequent"); - if (consequent.isJSXFragment() || consequent.isJSXElement()) { - processJSX(context, consequent); - } - const alternate = arg.get("alternate"); - if (alternate.isJSXFragment() || alternate.isJSXElement()) { - processJSX(context, alternate); - } - return; - } - - if (!arg.isJSXFragment() && !arg.isJSXElement()) { - return; - } - - jsxNode = arg; - } - - if (!jsxNode) { - return; - } - - processJSX(context, jsxNode); -} - -/** - * Recursively processes JSX elements to add Sentry tracking attributes. - * Handles both JSX elements and fragments, applying appropriate attributes - * based on configuration and component context. - */ -function processJSX( - context: JSXProcessingContext, - jsxNode: Babel.NodePath, - componentName?: string -): void { - if (!jsxNode) { - return; - } - - // Use provided componentName or fall back to context componentName - const currentComponentName = componentName ?? context.componentName; - const isRootElement = componentName === undefined; - - // NOTE: I don't know of a case where `openingElement` would have more than one item, - // but it's safer to always iterate - const paths = jsxNode.get("openingElement"); - const openingElements = Array.isArray(paths) ? paths : [paths]; - - openingElements.forEach((openingElement) => { - applyAttributes( - context, - openingElement as Babel.NodePath, - currentComponentName - ); - }); - - let children = jsxNode.get("children"); - // TODO: See why `Array.isArray` doesn't have correct behaviour here - if (children && !("length" in children)) { - // A single child was found, maybe a bit of static text - children = [children]; - } - - let shouldSetComponentName = context.annotateFragments; - - children.forEach((child) => { - // Happens for some node types like plain text - if (!child.node) { - return; - } - - // 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 - const openingElement = child.get("openingElement"); - // TODO: Improve this. We never expect to have multiple opening elements - // but if it's possible, this should work - if (Array.isArray(openingElement)) { - return; - } - - if (shouldSetComponentName && openingElement?.node) { - shouldSetComponentName = false; - processJSX(context, child, currentComponentName); - } else { - processJSX(context, child, ""); - } - }); - - if (isRootElement && context.autoInjectSentryLabel) { - maybeInjectSentryLabel(context, jsxNode); - } -} - -/** - * Applies Sentry tracking attributes to a JSX opening element. - * Adds component name, element name, and source file attributes while - * respecting ignore lists and fragment detection. - */ -function applyAttributes( - context: JSXProcessingContext, - openingElement: Babel.NodePath, - componentName: string -): void { - const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context; - const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames; - - // e.g., Raw JSX text like the `A` in `

a

` - if (!openingElement.node) { - return; - } - - // Check if this is a React fragment - if so, skip attribute addition entirely - const isFragment = isReactFragment(t, openingElement, fragmentContext); - if (isFragment) { - return; - } - - if (!openingElement.node.attributes) openingElement.node.attributes = []; - const elementName = getPathName(t, openingElement); - - const isAnIgnoredComponent = ignoredComponents.some( - (ignoredComponent) => ignoredComponent === componentName || ignoredComponent === elementName - ); - - // Add a stable attribute for the element name but only for non-DOM names - let isAnIgnoredElement = false; - if (!isAnIgnoredComponent && !hasAttributeWithName(openingElement, elementAttributeName)) { - if (DEFAULT_IGNORED_ELEMENTS.includes(elementName)) { - isAnIgnoredElement = true; - } else { - // Always add element attribute for non-ignored elements - if (elementAttributeName) { - openingElement.node.attributes.push( - t.jSXAttribute(t.jSXIdentifier(elementAttributeName), t.stringLiteral(elementName)) - ); - } - } - } - - // Add a stable attribute for the component name (absent for non-root elements) - if ( - componentName && - !isAnIgnoredComponent && - !hasAttributeWithName(openingElement, componentAttributeName) - ) { - if (componentAttributeName) { - openingElement.node.attributes.push( - t.jSXAttribute(t.jSXIdentifier(componentAttributeName), t.stringLiteral(componentName)) - ); - } - } - - // Add a stable attribute for the source file name - // Updated condition: add source file for elements that have either: - // 1. A component name (root elements), OR - // 2. An element name that's not ignored (child elements) - if ( - sourceFileName && - !isAnIgnoredComponent && - (componentName || !isAnIgnoredElement) && - !hasAttributeWithName(openingElement, sourceFileAttributeName) - ) { - if (sourceFileAttributeName) { - openingElement.node.attributes.push( - t.jSXAttribute(t.jSXIdentifier(sourceFileAttributeName), t.stringLiteral(sourceFileName)) - ); - } - } -} - -function sourceFileNameFromState(state: AnnotationPluginPass): string | undefined { - const name = fullSourceFileNameFromState(state); - if (!name) { - return undefined; - } - - if (name.indexOf("/") !== -1) { - return name.split("/").pop(); - } else if (name.indexOf("\\") !== -1) { - return name.split("\\").pop(); - } else { - return name; - } -} - -function fullSourceFileNameFromState(state: AnnotationPluginPass): string | null { - // @ts-expect-error This type is incorrect in Babel, `sourceFileName` is the correct type - const name = state.file.opts.parserOpts?.sourceFileName as unknown; - - if (typeof name === "string") { - return name; - } - - return null; -} - -function isKnownIncompatiblePluginFromState(state: AnnotationPluginPass): boolean { - const fullSourceFileName = fullSourceFileNameFromState(state); - - if (!fullSourceFileName) { - return false; - } - - return KNOWN_INCOMPATIBLE_PLUGINS.some((pluginName) => { - if ( - fullSourceFileName.includes(`/node_modules/${pluginName}/`) || - fullSourceFileName.includes(`\\node_modules\\${pluginName}\\`) - ) { - return true; - } - - return false; - }); -} - -function attributeNamesFromState(state: AnnotationPluginPass): [string, string, string] { - if (state.opts.native) { - return [nativeComponentName, nativeElementName, nativeSourceFileName]; - } - - return [webComponentName, webElementName, webSourceFileName]; -} - -function collectFragmentContext(programPath: Babel.NodePath): FragmentContext { - const fragmentAliases = new Set(); - const reactNamespaceAliases = new Set(["React"]); // Default React namespace - - programPath.traverse({ - ImportDeclaration(importPath) { - const source = importPath.node.source.value; - - // Handle React imports - if (source === "react" || source === "React") { - importPath.node.specifiers.forEach((spec) => { - if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") { - // Detect aliased React.Fragment imports (e.g., `Fragment as F`) - // so we can later identify as a fragment in JSX. - if (spec.imported.name === "Fragment") { - fragmentAliases.add(spec.local.name); - } - } else if ( - spec.type === "ImportDefaultSpecifier" || - spec.type === "ImportNamespaceSpecifier" - ) { - // import React from 'react' -> React OR - // import * as React from 'react' -> React - reactNamespaceAliases.add(spec.local.name); - } - }); - } - }, - - // Handle simple variable assignments only (avoid complex cases) - VariableDeclarator(varPath) { - if (varPath.node.init) { - const init = varPath.node.init; - - // Handle identifier assignments: const MyFragment = Fragment - if (varPath.node.id.type === "Identifier") { - // Handle: const MyFragment = Fragment (only if Fragment is a known alias) - if (init.type === "Identifier" && fragmentAliases.has(init.name)) { - fragmentAliases.add(varPath.node.id.name); - } - - // Handle: const MyFragment = React.Fragment (only for known React namespaces) - if ( - init.type === "MemberExpression" && - init.object.type === "Identifier" && - init.property.type === "Identifier" && - init.property.name === "Fragment" && - reactNamespaceAliases.has(init.object.name) - ) { - fragmentAliases.add(varPath.node.id.name); - } - } - - // Handle destructuring assignments: const { Fragment } = React - if (varPath.node.id.type === "ObjectPattern") { - if (init.type === "Identifier" && reactNamespaceAliases.has(init.name)) { - const properties = varPath.node.id.properties; - - for (const prop of properties) { - if ( - prop.type === "ObjectProperty" && - prop.key?.type === "Identifier" && - prop.value?.type === "Identifier" && - prop.key.name === "Fragment" - ) { - fragmentAliases.add(prop.value.name); - } - } - } - } - } - }, - }); - - return { fragmentAliases, reactNamespaceAliases }; -} - -function isReactFragment( - t: typeof Babel.types, - openingElement: Babel.NodePath, - context?: FragmentContext // Add this optional parameter -): boolean { - // Handle JSX fragments (<>) - if (openingElement.isJSXFragment()) { - return true; - } - - const elementName = getPathName(t, openingElement); - - // Direct fragment references - if (elementName === "Fragment" || elementName === "React.Fragment") { - return true; - } - - // TODO: All these objects are typed as unknown, maybe an oversight in Babel types? - - // Check if the element name is a known fragment alias - if (context && elementName && context.fragmentAliases.has(elementName)) { - return true; - } - - // Handle JSXMemberExpression - if ( - openingElement.node && - "name" in openingElement.node && - openingElement.node.name && - typeof openingElement.node.name === "object" && - "type" in openingElement.node.name && - openingElement.node.name.type === "JSXMemberExpression" - ) { - const nodeName = openingElement.node.name; - if (typeof nodeName !== "object" || !nodeName) { - return false; - } - - if ("object" in nodeName && "property" in nodeName) { - const nodeNameObject = nodeName.object; - const nodeNameProperty = nodeName.property; - - if (typeof nodeNameObject !== "object" || typeof nodeNameProperty !== "object") { - return false; - } - - if (!nodeNameObject || !nodeNameProperty) { - return false; - } - - const objectName = "name" in nodeNameObject && nodeNameObject.name; - const propertyName = "name" in nodeNameProperty && nodeNameProperty.name; - - // React.Fragment check - if (objectName === "React" && propertyName === "Fragment") { - return true; - } - - // Enhanced checks using context - if (context) { - // Check React.Fragment pattern with known React namespaces - if ( - context.reactNamespaceAliases.has(objectName as string) && - propertyName === "Fragment" - ) { - return true; - } - - // Check MyFragment.Fragment pattern - if (context.fragmentAliases.has(objectName as string) && propertyName === "Fragment") { - return true; - } - } - } - } - - return false; -} - -function hasAttributeWithName( - openingElement: Babel.NodePath, - name: string | undefined | null -): boolean { - if (!name) { - return false; - } - - return openingElement.node.attributes.some((node) => { - if (node.type === "JSXAttribute") { - return node.name.name === name; - } - - return false; - }); -} - -function getPathName(t: typeof Babel.types, path: Babel.NodePath): string { - if (!path.node) return UNKNOWN_ELEMENT_NAME; - if (!("name" in path.node)) { - return UNKNOWN_ELEMENT_NAME; - } - - const name = path.node.name; - - if (typeof name === "string") { - return name; - } - - if (t.isIdentifier(name) || t.isJSXIdentifier(name)) { - return name.name; - } - - if (t.isJSXNamespacedName(name)) { - return name.name.name; - } - - // Handle JSX member expressions like Tab.Group - if (t.isJSXMemberExpression(name)) { - const objectName = getJSXMemberExpressionObjectName(t, name.object); - const propertyName = name.property.name; - return `${objectName}.${propertyName}`; - } - - return UNKNOWN_ELEMENT_NAME; -} - -// Recursively handle nested member expressions (e.g. Components.UI.Header) -function getJSXMemberExpressionObjectName( - t: typeof Babel.types, - object: Babel.types.JSXMemberExpression | Babel.types.JSXIdentifier -): string { - if (t.isJSXIdentifier(object)) { - return object.name; - } - if (t.isJSXMemberExpression(object)) { - const objectName = getJSXMemberExpressionObjectName(t, object.object); - return `${objectName}.${object.property.name}`; - } - - return UNKNOWN_ELEMENT_NAME; -} - -/** - * Extracts static text content from JSX children, searching up to a depth limit. - * Collects text from JSXText nodes of the root element and from recognized - * text components (e.g. ). Non-text custom components are traversed - * but their own JSXText is not collected. - * - * Returns null when dynamic content is found anywhere in the subtree, - * signaling that the entire label should be skipped. - */ -function extractStaticTextFromChildren( - t: typeof Babel.types, - node: Babel.types.JSXElement | Babel.types.JSXFragment, - textComponentNames: string[], - depth: number, - isRoot: boolean -): string[] | null { - if (depth <= 0) { - return []; - } - - const texts: string[] = []; - - for (const child of node.children) { - if (t.isJSXText(child)) { - if (isRoot) { - const trimmed = child.value.replace(/\s+/g, " ").trim(); - if (trimmed) { - texts.push(trimmed); - } - } - } else if (t.isJSXElement(child)) { - const childName = getElementName(t, child.openingElement); - - if (textComponentNames.includes(childName)) { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - texts.push(...innerTexts); - } else { - const result = extractStaticTextFromChildren( - t, - child, - textComponentNames, - depth - 1, - false - ); - if (result === null) { - return null; - } - texts.push(...result); - } - } else if (t.isJSXFragment(child)) { - const result = extractStaticTextFromChildren(t, child, textComponentNames, depth, isRoot); - if (result === null) { - return null; - } - texts.push(...result); - } else if (t.isJSXExpressionContainer(child)) { - if (!t.isJSXEmptyExpression(child.expression)) { - return null; - } - } else if (t.isJSXSpreadChild(child)) { - return null; - } - } - - return texts; -} - -/** - * Recursively extracts static text from within a recognized text component. - * Handles nested text components (e.g. Hello world) - * which is the standard React Native pattern for inline styling. - * - * Returns null when any dynamic content is found, signaling bail-out. - */ -function extractTextFromTextComponent( - t: typeof Babel.types, - node: Babel.types.JSXElement | Babel.types.JSXFragment, - textComponentNames: string[] -): string[] | null { - const texts: string[] = []; - - for (const child of node.children) { - if (t.isJSXText(child)) { - const trimmed = child.value.replace(/\s+/g, " ").trim(); - if (trimmed) { - texts.push(trimmed); - } - } else if (t.isJSXExpressionContainer(child)) { - if (!t.isJSXEmptyExpression(child.expression)) { - return null; - } - } else if (t.isJSXElement(child)) { - const childName = getElementName(t, child.openingElement); - if (textComponentNames.includes(childName)) { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - texts.push(...innerTexts); - } else { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - } - } else if (t.isJSXFragment(child)) { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - texts.push(...innerTexts); - } else if (t.isJSXSpreadChild(child)) { - return null; - } - } - - return texts; -} - -function getElementName( - t: typeof Babel.types, - openingElement: Babel.types.JSXOpeningElement -): string { - const name = openingElement.name; - if (t.isJSXIdentifier(name)) { - return name.name; - } - if (t.isJSXMemberExpression(name)) { - return `${getJSXMemberExpressionObjectName(t, name.object)}.${name.property.name}`; - } - return ""; -} - -/** - * Injects a sentry-label attribute on the root JSX element of a component if - * static text content can be extracted from its children. - * - * When the root is a JSX fragment, the first JSXElement child is used as the - * target for both text extraction and attribute injection (since fragments - * cannot carry attributes). - */ -function maybeInjectSentryLabel(context: JSXProcessingContext, jsxNode: Babel.NodePath): void { - const { t, textComponentNames, ignoredComponents, componentName } = context; - const node = jsxNode.node; - - let targetElement: Babel.types.JSXElement; - - if (t.isJSXElement(node)) { - targetElement = node; - } else if (t.isJSXFragment(node)) { - const firstChild = node.children.find((c): c is Babel.types.JSXElement => t.isJSXElement(c)); - if (!firstChild) { - return; - } - targetElement = firstChild; - } else { - return; - } - - const targetElementName = getElementName(t, targetElement.openingElement); - - if ( - ignoredComponents.some((ignored) => ignored === componentName || ignored === targetElementName) - ) { - return; - } - - if ( - targetElement.openingElement.attributes.some( - (attr) => t.isJSXAttribute(attr) && attr.name.name === SENTRY_LABEL_ATTRIBUTE - ) - ) { - return; - } - - const texts = extractStaticTextFromChildren( - t, - targetElement, - textComponentNames, - MAX_TEXT_SEARCH_DEPTH, - true - ); - - if (texts === null) { - return; - } - - let label = texts.join(" ").replace(/\s+/g, " ").trim(); - - if (!label) { - return; - } - - if (label.length > MAX_LABEL_LENGTH) { - label = `${label.substring(0, MAX_LABEL_LENGTH - 3)}...`; - } - - targetElement.openingElement.attributes.push( - t.jSXAttribute(t.jSXIdentifier(SENTRY_LABEL_ATTRIBUTE), t.stringLiteral(label)) - ); -} - -const UNKNOWN_ELEMENT_NAME = "unknown"; diff --git a/packages/bundler-plugins/src/core/build-plugin-manager.ts b/packages/bundler-plugins/src/core/build-plugin-manager.ts deleted file mode 100644 index 7a1b5f87..00000000 --- a/packages/bundler-plugins/src/core/build-plugin-manager.ts +++ /dev/null @@ -1,879 +0,0 @@ -/* oxlint-disable max-lines */ -import SentryCli from "@sentry/cli"; -import { - closeSession, - DEFAULT_ENVIRONMENT, - getTraceData, - makeSession, - setMeasurement, - startSpan, -} from "@sentry/core"; -import * as dotenv from "dotenv"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import type { NormalizedOptions } from "./options-mapping"; -import { normalizeUserOptions, validateOptions } from "./options-mapping"; -import type { Logger } from "./logger"; -import { createLogger } from "./logger"; -import { - allowedToSendTelemetry, - createSentryInstance, - safeFlushTelemetry, -} from "./sentry/telemetry"; -import type { Options, SentrySDKBuildFlags } from "./types"; -import { - arrayify, - getProjects, - getTurborepoEnvPassthroughWarning, - serializeIgnoreOptions, - stripQueryAndHashFromPath, -} from "./utils"; -import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from "./debug-id-upload"; -import { globFiles } from "./glob"; -import { LIB_VERSION } from "./version"; - -// Module-level guard to prevent duplicate deploy records when multiple bundler plugin -// instances run in the same process (e.g. Next.js creates separate webpack compilers -// for client, server, and edge). Keyed by release name. -const _deployedReleases = new Set(); - -/** @internal Exported for testing only. */ -export function _resetDeployedReleasesForTesting(): void { - _deployedReleases.clear(); -} - -export type SentryBuildPluginManager = { - /** - * A logger instance that takes the options passed to the build plugin manager into account. (for silencing and log level etc.) - */ - logger: Logger; - - /** - * Options after normalization. Includes things like the inferred release name. - */ - normalizedOptions: NormalizedOptions; - /** - * Magic strings and their replacement values that can be used for bundle size optimizations. This already takes - * into account the options passed to the build plugin manager. - */ - bundleSizeOptimizationReplacementValues: SentrySDKBuildFlags; - /** - * Metadata that should be injected into bundles if possible. Takes into account options passed to the build plugin manager. - */ - // See `generateModuleMetadataInjectorCode` for how this should be used exactly - bundleMetadata: Record; - - /** - * Contains utility functions for emitting telemetry via the build plugin manager. - */ - telemetry: { - /** - * Emits a `Sentry Bundler Plugin execution` signal. - */ - emitBundlerPluginExecutionSignal(): Promise; - }; - - /** - * Will potentially create a release based on the build plugin manager options. - * - * Also - * - finalizes the release - * - sets commits - * - uploads legacy sourcemaps - * - adds deploy information - */ - createRelease(): Promise; - - /** - * Injects debug IDs into the build artifacts. - * - * This is a separate function from `uploadSourcemaps` because that needs to run before the sourcemaps are uploaded. - * Usually the respective bundler-plugin will take care of this before the sourcemaps are uploaded. - * Only use this if you need to manually inject debug IDs into the build artifacts. - */ - injectDebugIds(buildArtifactPaths: string[]): Promise; - - /** - * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded - */ - uploadSourcemaps( - buildArtifactPaths: string[], - opts?: { prepareArtifacts?: boolean } - ): Promise; - - /** - * Will delete artifacts based on the passed `sourcemaps.filesToDeleteAfterUpload` option. - */ - deleteArtifacts(): Promise; - - createDependencyOnBuildArtifacts: () => () => void; -}; - -function createCliInstance(options: NormalizedOptions): SentryCli { - return new SentryCli(null, { - authToken: options.authToken, - org: options.org, - // Default to the first project if multiple projects are specified - project: getProjects(options.project)?.[0], - silent: options.silent, - url: options.url, - vcsRemote: options.release.vcsRemote, - headers: { - ...(options.telemetry ? getTraceData() : {}), - ...options.headers, - }, - }); -} - -/** - * Creates a build plugin manager that exposes primitives for everything that a Sentry JavaScript SDK or build tooling may do during a build. - * - * The build plugin manager's behavior strongly depends on the options that are passed in. - */ -export function createSentryBuildPluginManager( - userOptions: Options, - bundlerPluginMetaContext: { - /** - * E.g. `webpack` or `nextjs` or `turbopack` - */ - buildTool: string; - /** - * E.g. `5` for webpack v5 or `4` for Rollup v4 - */ - buildToolMajorVersion?: string; - /** - * E.g. `[sentry-webpack-plugin]` or `[@sentry/nextjs]` - */ - loggerPrefix: string; - } -): SentryBuildPluginManager { - const logger = createLogger({ - prefix: bundlerPluginMetaContext.loggerPrefix, - silent: userOptions.silent ?? false, - debug: userOptions.debug ?? false, - }); - - try { - const dotenvFile = fs.readFileSync( - path.join(process.cwd(), ".env.sentry-build-plugin"), - "utf-8" - ); - // NOTE: Do not use the dotenv.config API directly to read the dotenv file! For some ungodly reason, it falls back to reading `${process.cwd()}/.env` which is absolutely not what we want. - const dotenvResult = dotenv.parse(dotenvFile); - - // Vite has a bug/behaviour where spreading into process.env will cause it to crash - // https://github.com/vitest-dev/vitest/issues/1870#issuecomment-1501140251 - Object.assign(process.env, dotenvResult); - - logger.info('Using environment variables configured in ".env.sentry-build-plugin".'); - } catch (e: unknown) { - // Ignore "file not found" errors but throw all others - if (typeof e === "object" && e && "code" in e && e.code !== "ENOENT") { - throw e; - } - } - - const options = normalizeUserOptions(userOptions); - - if (options.disable) { - // Early-return a noop build plugin manager instance so that we - // don't continue validating options, setting up Sentry, etc. - // Otherwise we might create side-effects or log messages that - // users don't expect from a disabled plugin. - return { - normalizedOptions: options, - logger, - bundleSizeOptimizationReplacementValues: {}, - telemetry: { - emitBundlerPluginExecutionSignal: async () => { - /* noop */ - }, - }, - bundleMetadata: {}, - createRelease: async () => { - /* noop */ - }, - uploadSourcemaps: async () => { - /* noop */ - }, - deleteArtifacts: async () => { - /* noop */ - }, - createDependencyOnBuildArtifacts: () => () => { - /* noop */ - }, - injectDebugIds: async () => { - /* noop */ - }, - }; - } - - const shouldSendTelemetry = allowedToSendTelemetry(options); - const { sentryScope, sentryClient } = createSentryInstance( - options, - shouldSendTelemetry, - bundlerPluginMetaContext.buildTool, - bundlerPluginMetaContext.buildToolMajorVersion - ); - - const { release, environment = DEFAULT_ENVIRONMENT } = sentryClient.getOptions(); - - const sentrySession = makeSession({ release, environment }); - sentryScope.setSession(sentrySession); - // Send the start of the session - sentryClient.captureSession(sentrySession); - - let sessionHasEnded = false; // Just to prevent infinite loops with beforeExit, which is called whenever the event loop empties out - - function endSession(): void { - if (sessionHasEnded) { - return; - } - - closeSession(sentrySession); - sentryClient.captureSession(sentrySession); - sessionHasEnded = true; - } - - // We also need to manually end sessions on errors because beforeExit is not called on crashes - process.on("beforeExit", () => { - endSession(); - }); - - // Set the User-Agent that Sentry CLI will use when interacting with Sentry - process.env["SENTRY_PIPELINE"] = `${bundlerPluginMetaContext.buildTool}-plugin/${LIB_VERSION}`; - - // Propagate debug flag to Sentry CLI via environment variable - // Only set if not already defined to respect user's explicit configuration - if (options.debug && !process.env["SENTRY_LOG_LEVEL"]) { - process.env["SENTRY_LOG_LEVEL"] = "debug"; - } - - // Not a bulletproof check but should be good enough to at least sometimes determine - // if the plugin is called in dev/watch mode or for a prod build. The important part - // here is to avoid a false positive. False negatives are okay. - const isDevMode = process.env["NODE_ENV"] === "development"; - - /** - * Handles errors caught and emitted in various areas of the plugin. - * - * Also sets the sentry session status according to the error handling. - * - * If users specify their custom `errorHandler` we'll leave the decision to throw - * or continue up to them. By default, @param throwByDefault controls if the plugin - * should throw an error (which causes a build fail in most bundlers) or continue. - */ - function handleRecoverableError(unknownError: unknown, throwByDefault: boolean): void { - sentrySession.status = "abnormal"; - try { - if (options.errorHandler) { - try { - if (unknownError instanceof Error) { - options.errorHandler(unknownError); - } else { - options.errorHandler(new Error("An unknown error occurred")); - } - } catch (e) { - sentrySession.status = "crashed"; - throw e; - } - } else { - // setting the session to "crashed" b/c from a plugin perspective this run failed. - // However, we're intentionally not rethrowing the error to avoid breaking the user build. - sentrySession.status = "crashed"; - if (throwByDefault) { - throw unknownError; - } - logger.error("An error occurred. Couldn't finish all operations:", unknownError); - } - } finally { - endSession(); - } - } - - if (!validateOptions(options, logger)) { - // Throwing by default to avoid a misconfigured plugin going unnoticed. - handleRecoverableError( - new Error("Options were not set correctly. See output above for more details."), - true - ); - } - - // We have multiple plugins depending on generated source map files. (debug ID upload, legacy upload) - // Additionally, we also want to have the functionality to delete files after uploading sourcemaps. - // All of these plugins and the delete functionality need to run in the same hook (`writeBundle`). - // Since the plugins among themselves are not aware of when they run and finish, we need a system to - // track their dependencies on the generated files, so that we can initiate the file deletion only after - // nothing depends on the files anymore. - const dependenciesOnBuildArtifacts = new Set(); - const buildArtifactsDependencySubscribers: (() => void)[] = []; - - function notifyBuildArtifactDependencySubscribers(): void { - buildArtifactsDependencySubscribers.forEach((subscriber) => { - subscriber(); - }); - } - - function createDependencyOnBuildArtifacts(): () => void { - const dependencyIdentifier = Symbol(); - dependenciesOnBuildArtifacts.add(dependencyIdentifier); - - return function freeDependencyOnBuildArtifacts() { - dependenciesOnBuildArtifacts.delete(dependencyIdentifier); - notifyBuildArtifactDependencySubscribers(); - }; - } - - /** - * Returns a Promise that resolves when all the currently active dependencies are freed again. - * - * It is very important that this function is called as late as possible before wanting to await the Promise to give - * the dependency producers as much time as possible to register themselves. - */ - function waitUntilBuildArtifactDependenciesAreFreed(): Promise { - return new Promise((resolve) => { - buildArtifactsDependencySubscribers.push(() => { - if (dependenciesOnBuildArtifacts.size === 0) { - resolve(); - } - }); - - if (dependenciesOnBuildArtifacts.size === 0) { - resolve(); - } - }); - } - - const bundleSizeOptimizationReplacementValues: SentrySDKBuildFlags = {}; - if (options.bundleSizeOptimizations) { - const { bundleSizeOptimizations } = options; - - if (bundleSizeOptimizations.excludeDebugStatements) { - bundleSizeOptimizationReplacementValues["__SENTRY_DEBUG__"] = false; - } - if (bundleSizeOptimizations.excludeTracing) { - bundleSizeOptimizationReplacementValues["__SENTRY_TRACING__"] = false; - } - if (bundleSizeOptimizations.excludeReplayCanvas) { - bundleSizeOptimizationReplacementValues["__RRWEB_EXCLUDE_CANVAS__"] = true; - } - if (bundleSizeOptimizations.excludeReplayIframe) { - bundleSizeOptimizationReplacementValues["__RRWEB_EXCLUDE_IFRAME__"] = true; - } - if (bundleSizeOptimizations.excludeReplayShadowDom) { - bundleSizeOptimizationReplacementValues["__RRWEB_EXCLUDE_SHADOW_DOM__"] = true; - } - if (bundleSizeOptimizations.excludeReplayWorker) { - bundleSizeOptimizationReplacementValues["__SENTRY_EXCLUDE_REPLAY_WORKER__"] = true; - } - } - - let bundleMetadata: Record = {}; - if (options.moduleMetadata || options.applicationKey) { - if (options.applicationKey) { - // We use different keys so that if user-code receives multiple bundling passes, we will store the application keys of all the passes. - // It is a bit unfortunate that we have to inject the metadata snippet at the top, because after multiple - // injections, the first injection will always "win" because it comes last in the code. We would generally be - // fine with making the last bundling pass win. But because it cannot win, we have to use a workaround of storing - // the app keys in different object keys. - // We can simply use the `_sentryBundlerPluginAppKey:` to filter for app keys in the SDK. - bundleMetadata[`_sentryBundlerPluginAppKey:${options.applicationKey}`] = true; - } - - if (typeof options.moduleMetadata === "function") { - const args = { - org: options.org, - project: getProjects(options.project)?.[0], - projects: getProjects(options.project), - release: options.release.name, - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - bundleMetadata = { ...bundleMetadata, ...options.moduleMetadata(args) }; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - bundleMetadata = { ...bundleMetadata, ...options.moduleMetadata }; - } - } - - return { - /** - * A logger instance that takes the options passed to the build plugin manager into account. (for silencing and log level etc.) - */ - logger, - - /** - * Options after normalization. Includes things like the inferred release name. - */ - normalizedOptions: options, - - /** - * Magic strings and their replacement values that can be used for bundle size optimizations. This already takes - * into account the options passed to the build plugin manager. - */ - bundleSizeOptimizationReplacementValues, - - /** - * Metadata that should be injected into bundles if possible. Takes into account options passed to the build plugin manager. - */ - // See `generateModuleMetadataInjectorCode` for how this should be used exactly - bundleMetadata, - - /** - * Contains utility functions for emitting telemetry via the build plugin manager. - */ - telemetry: { - /** - * Emits a `Sentry Bundler Plugin execution` signal. - */ - async emitBundlerPluginExecutionSignal() { - if (await shouldSendTelemetry) { - logger.info( - "Sending telemetry data on issues and performance to Sentry. To disable telemetry, set `options.telemetry` to `false`." - ); - startSpan({ name: "Sentry Bundler Plugin execution", scope: sentryScope }, () => { - // - }); - await safeFlushTelemetry(sentryClient); - } - }, - }, - - /** - * Will potentially create a release based on the build plugin manager options. - * - * Also - * - finalizes the release - * - sets commits - * - uploads legacy sourcemaps - * - adds deploy information - */ - async createRelease() { - if (!options.release.name) { - logger.debug( - "No release name provided. Will not create release. Please set the `release.name` option to identify your release." - ); - return; - } else if (isDevMode) { - logger.debug("Running in development mode. Will not create release."); - return; - } else if (!options.authToken) { - logger.warn( - `No auth token provided. Will not create release. Please set the \`authToken\` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/${getTurborepoEnvPassthroughWarning("SENTRY_AUTH_TOKEN")}` - ); - return; - } else if (!options.org && !options.authToken.startsWith("sntrys_")) { - logger.warn( - `No organization slug provided. Will not create release. Please set the \`org\` option to your Sentry organization slug.${getTurborepoEnvPassthroughWarning("SENTRY_ORG")}` - ); - return; - } else if ( - !options.project || - (Array.isArray(options.project) && options.project.length === 0) - ) { - logger.warn( - `No project provided. Will not create release. Please set the \`project\` option to your Sentry project slug.${getTurborepoEnvPassthroughWarning("SENTRY_PROJECT")}` - ); - return; - } - - // It is possible that this writeBundle hook is called multiple times in one build (for example when reusing the plugin, or when using build tooling like `@vitejs/plugin-legacy`) - // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files. - const freeWriteBundleInvocationDependencyOnSourcemapFiles = - createDependencyOnBuildArtifacts(); - - try { - const cliInstance = createCliInstance(options); - - if (options.release.create) { - const releaseOutput = await cliInstance.releases.new(options.release.name); - logger.debug("Release created:", releaseOutput); - } - - if (options.release.uploadLegacySourcemaps) { - const normalizedInclude = arrayify(options.release.uploadLegacySourcemaps) - .map((includeItem) => - typeof includeItem === "string" ? { paths: [includeItem] } : includeItem - ) - .map((includeEntry) => ({ - ...includeEntry, - validate: includeEntry.validate ?? false, - ext: includeEntry.ext - ? includeEntry.ext.map((extension) => `.${extension.replace(/^\./, "")}`) - : [".js", ".map", ".jsbundle", ".bundle"], - ignore: includeEntry.ignore ? arrayify(includeEntry.ignore) : undefined, - })); - - await cliInstance.releases.uploadSourceMaps(options.release.name, { - include: normalizedInclude, - dist: options.release.dist, - projects: getProjects(options.project), - // We want this promise to throw if the sourcemaps fail to upload so that we know about it. - // see: https://github.com/getsentry/sentry-cli/pull/2605 - live: "rejectOnError", - }); - } - - if (options.release.setCommits !== false) { - try { - await cliInstance.releases.setCommits( - options.release.name, - // set commits always exists due to the normalize function - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - options.release.setCommits! - ); - } catch (e) { - // shouldNotThrowOnFailure being present means that the plugin defaulted to `{ auto: true }` for the setCommitsOptions, meaning that wee should not throw when CLI throws because there is no repo - if ( - options.release.setCommits && - "shouldNotThrowOnFailure" in options.release.setCommits && - options.release.setCommits.shouldNotThrowOnFailure - ) { - logger.debug( - "An error occurred setting commits on release (this message can be ignored unless you commits on release are desired):", - e - ); - } else { - throw e; - } - } - } - - if (options.release.finalize) { - await cliInstance.releases.finalize(options.release.name); - } - - if (options.release.deploy && !_deployedReleases.has(options.release.name)) { - await cliInstance.releases.newDeploy(options.release.name, options.release.deploy); - _deployedReleases.add(options.release.name); - } - } catch (e) { - sentryScope.captureException('Error in "releaseManagementPlugin" writeBundle hook'); - await safeFlushTelemetry(sentryClient); - handleRecoverableError(e, false); - } finally { - freeWriteBundleInvocationDependencyOnSourcemapFiles(); - } - }, - - /* - Injects debug IDs into the build artifacts. - - This is a separate function from `uploadSourcemaps` because that needs to run before the sourcemaps are uploaded. - Usually the respective bundler-plugin will take care of this before the sourcemaps are uploaded. - Only use this if you need to manually inject debug IDs into the build artifacts. - */ - async injectDebugIds(buildArtifactPaths: string[]) { - await startSpan( - { name: "inject-debug-ids", scope: sentryScope, forceTransaction: true }, - async () => { - try { - const cliInstance = createCliInstance(options); - await cliInstance.execute( - [ - "sourcemaps", - "inject", - ...serializeIgnoreOptions(options.sourcemaps?.ignore), - ...buildArtifactPaths, - ], - options.debug ? "rejectOnError" : false - ); - } catch (e) { - sentryScope.captureException('Error in "debugIdInjectionPlugin" writeBundle hook'); - handleRecoverableError(e, false); - } finally { - await safeFlushTelemetry(sentryClient); - } - } - ); - }, - - /** - * Uploads sourcemaps using the "Debug ID" method. - * - * By default, this prepares bundles in a temporary folder before uploading. You can opt into an - * in-place, direct upload path by setting `prepareArtifacts` to `false`. If `prepareArtifacts` is set to - * `false`, no preparation (e.g. adding `//# debugId=...` and writing adjusted source maps) is performed and no temp folder is used. - * - * @param buildArtifactPaths - The paths of the build artifacts to upload - * @param opts - Optional flags to control temp folder usage and preparation - */ - async uploadSourcemaps(buildArtifactPaths: string[], opts?: { prepareArtifacts?: boolean }) { - if (!canUploadSourceMaps(options, logger, isDevMode)) { - return; - } - - // Early exit if assets is explicitly set to an empty array - const assets = options.sourcemaps?.assets; - if (Array.isArray(assets) && assets.length === 0) { - logger.debug( - "Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID." - ); - return; - } - - await startSpan( - // This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions. - { name: "debug-id-sourcemap-upload", scope: sentryScope, forceTransaction: true }, - async () => { - // If we're not using a temp folder, we must not prepare artifacts in-place (to avoid mutating user files) - const shouldPrepare = opts?.prepareArtifacts ?? true; - - let folderToCleanUp: string | undefined; - - // It is possible that this writeBundle hook (which calls this function) is called multiple times in one build (for example when reusing the plugin, or when using build tooling like `@vitejs/plugin-legacy`) - // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files. - const freeUploadDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); - - try { - if (!shouldPrepare) { - // Direct CLI upload from existing artifact paths (no globbing, no preparation) - let pathsToUpload: string[]; - - if (assets) { - pathsToUpload = Array.isArray(assets) ? assets : [assets]; - logger.debug( - `Direct upload mode: passing user-provided assets directly to CLI: ${pathsToUpload.join( - ", " - )}` - ); - } else { - // Use original paths e.g. like ['.next/server'] directly –> preferred way when no globbing is done - pathsToUpload = buildArtifactPaths; - } - - const ignorePaths = options.sourcemaps?.ignore - ? Array.isArray(options.sourcemaps?.ignore) - ? options.sourcemaps?.ignore - : [options.sourcemaps?.ignore] - : []; - await startSpan({ name: "upload", scope: sentryScope }, async () => { - const cliInstance = createCliInstance(options); - await cliInstance.releases.uploadSourceMaps(options.release.name ?? "undefined", { - include: [ - { - paths: pathsToUpload, - rewrite: true, - dist: options.release.dist, - }, - ], - ignore: ignorePaths, - projects: getProjects(options.project), - live: "rejectOnError", - }); - }); - - logger.info("Successfully uploaded source maps to Sentry"); - } else { - // Prepare artifacts in temp folder before uploading - let globAssets: string | string[]; - if (assets) { - globAssets = assets; - } else { - logger.debug( - "No `sourcemaps.assets` option provided, falling back to uploading detected build artifacts." - ); - globAssets = buildArtifactPaths; - } - - const globResult = await startSpan( - { name: "glob", scope: sentryScope }, - async () => await globFiles(globAssets, { ignore: options.sourcemaps?.ignore }) - ); - - const debugIdChunkFilePaths = globResult.filter((debugIdChunkFilePath) => { - return !!stripQueryAndHashFromPath(debugIdChunkFilePath).match(/\.(js|mjs|cjs)$/); - }); - - // The order of the files output by glob() is not deterministic - // Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent - debugIdChunkFilePaths.sort(); - - if (debugIdChunkFilePaths.length === 0) { - logger.warn( - "Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option." - ); - } else { - const tmpUploadFolder = await startSpan( - { name: "mkdtemp", scope: sentryScope }, - async () => { - return ( - process.env?.["SENTRY_TEST_OVERRIDE_TEMP_DIR"] || - (await fs.promises.mkdtemp( - path.join(os.tmpdir(), "sentry-bundler-plugin-upload-") - )) - ); - } - ); - folderToCleanUp = tmpUploadFolder; - - // Prepare into temp folder, then upload - await startSpan( - { name: "prepare-bundles", scope: sentryScope }, - async (prepBundlesSpan) => { - // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so - // instead we do it with a maximum of 16 concurrent workers - const preparationTasks = debugIdChunkFilePaths.map( - (chunkFilePath, chunkIndex) => async () => { - await prepareBundleForDebugIdUpload( - chunkFilePath, - tmpUploadFolder, - chunkIndex, - logger, - options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook, - options.sourcemaps?.resolveSourceMap - ); - } - ); - const workers: Promise[] = []; - const worker = async (): Promise => { - while (preparationTasks.length > 0) { - const task = preparationTasks.shift(); - if (task) { - await task(); - } - } - }; - for (let workerIndex = 0; workerIndex < 16; workerIndex++) { - workers.push(worker()); - } - - await Promise.all(workers); - - const files = await fs.promises.readdir(tmpUploadFolder); - const stats = files.map((file) => - fs.promises.stat(path.join(tmpUploadFolder, file)) - ); - const uploadSize = (await Promise.all(stats)).reduce( - (accumulator, { size }) => accumulator + size, - 0 - ); - - setMeasurement("files", files.length, "none", prepBundlesSpan); - setMeasurement("upload_size", uploadSize, "byte", prepBundlesSpan); - - await startSpan({ name: "upload", scope: sentryScope }, async () => { - const cliInstance = createCliInstance(options); - await cliInstance.releases.uploadSourceMaps( - options.release.name ?? "undefined", - { - include: [ - { - paths: [tmpUploadFolder], - rewrite: false, - dist: options.release.dist, - }, - ], - projects: getProjects(options.project), - live: "rejectOnError", - } - ); - }); - } - ); - - logger.info("Successfully uploaded source maps to Sentry"); - } - } - } catch (e) { - sentryScope.captureException('Error in "debugIdUploadPlugin" writeBundle hook'); - handleRecoverableError(e, false); - } finally { - if (folderToCleanUp && !process.env?.["SENTRY_TEST_OVERRIDE_TEMP_DIR"]) { - logger.debug("Cleaning up temporary files..."); - void startSpan({ name: "cleanup", scope: sentryScope }, async () => { - if (folderToCleanUp) { - await fs.promises.rm(folderToCleanUp, { recursive: true, force: true }); - logger.debug(`Temporary folder deleted: ${folderToCleanUp}`); - } - }); - } - logger.debug("Freeing upload dependencies..."); - freeUploadDependencyOnBuildArtifacts(); - logger.debug("Flushing telemetry data..."); - await safeFlushTelemetry(sentryClient); - logger.debug("Telemetry flushed. Plugin upload process complete."); - } - } - ); - }, - - /** - * Will delete artifacts based on the passed `sourcemaps.filesToDeleteAfterUpload` option. - */ - async deleteArtifacts() { - try { - const filesToDelete = await options.sourcemaps?.filesToDeleteAfterUpload; - if (filesToDelete !== undefined) { - const filePathsToDelete = await globFiles(filesToDelete); - - logger.debug( - "Waiting for dependencies on generated files to be freed before deleting..." - ); - - await waitUntilBuildArtifactDependenciesAreFreed(); - - filePathsToDelete.forEach((filePathToDelete) => { - logger.debug(`Deleting asset after upload: ${filePathToDelete}`); - }); - - await Promise.all( - filePathsToDelete.map((filePathToDelete) => - fs.promises.rm(filePathToDelete, { force: true }).catch((e) => { - // This is allowed to fail - we just don't do anything - logger.debug( - `An error occurred while attempting to delete asset: ${filePathToDelete}`, - e - ); - }) - ) - ); - } - } catch (e) { - sentryScope.captureException('Error in "sentry-file-deletion-plugin" buildEnd hook'); - await safeFlushTelemetry(sentryClient); - // We throw by default if we get here b/c not being able to delete - // source maps could leak them to production - handleRecoverableError(e, true); - } - }, - createDependencyOnBuildArtifacts, - }; -} - -function canUploadSourceMaps( - options: NormalizedOptions, - logger: Logger, - isDevMode: boolean -): boolean { - if (options.sourcemaps?.disable) { - logger.debug( - "Source map upload was disabled. Will not upload sourcemaps using debug ID process." - ); - return false; - } - if (isDevMode) { - logger.debug("Running in development mode. Will not upload sourcemaps."); - return false; - } - if (!options.authToken) { - logger.warn( - `No auth token provided. Will not upload source maps. Please set the \`authToken\` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/${getTurborepoEnvPassthroughWarning("SENTRY_AUTH_TOKEN")}` - ); - return false; - } - if (!options.org && !options.authToken.startsWith("sntrys_")) { - logger.warn( - `No org provided. Will not upload source maps. Please set the \`org\` option to your Sentry organization slug.${getTurborepoEnvPassthroughWarning("SENTRY_ORG")}` - ); - return false; - } - if (!getProjects(options.project)?.[0]) { - logger.warn( - `No project provided. Will not upload source maps. Please set the \`project\` option to your Sentry project slug.${getTurborepoEnvPassthroughWarning("SENTRY_PROJECT")}` - ); - return false; - } - - return true; -} diff --git a/packages/bundler-plugins/src/core/debug-id-upload.ts b/packages/bundler-plugins/src/core/debug-id-upload.ts deleted file mode 100644 index a078719f..00000000 --- a/packages/bundler-plugins/src/core/debug-id-upload.ts +++ /dev/null @@ -1,245 +0,0 @@ -import fs from "fs"; -import path from "path"; -import * as url from "url"; -import * as util from "util"; -import { promisify } from "util"; -import type { SentryBuildPluginManager } from "./build-plugin-manager"; -import type { Logger } from "./logger"; -import type { ResolveSourceMapHook, RewriteSourcesHook } from "./types"; -import { stripQueryAndHashFromPath } from "./utils"; - -interface DebugIdUploadPluginOptions { - sentryBuildPluginManager: SentryBuildPluginManager; -} - -export function createDebugIdUploadFunction({ - sentryBuildPluginManager, -}: DebugIdUploadPluginOptions) { - return async (buildArtifactPaths: string[]) => { - // Webpack and perhaps other bundlers allow you to append query strings to - // filenames for cache busting purposes. We should strip these before upload. - const cleanedPaths = buildArtifactPaths.map(stripQueryAndHashFromPath); - await sentryBuildPluginManager.uploadSourcemaps(cleanedPaths); - }; -} - -export async function prepareBundleForDebugIdUpload( - bundleFilePath: string, - uploadFolder: string, - chunkIndex: number, - logger: Logger, - rewriteSourcesHook: RewriteSourcesHook, - resolveSourceMapHook: ResolveSourceMapHook | undefined -): Promise { - let bundleContent; - try { - bundleContent = await promisify(fs.readFile)(bundleFilePath, "utf8"); - } catch (e) { - logger.error( - `Could not read bundle to determine debug ID and source map: ${bundleFilePath}`, - e - ); - return; - } - - const debugId = determineDebugIdFromBundleSource(bundleContent); - if (debugId === undefined) { - logger.debug( - `Could not determine debug ID from bundle. This can happen if you did not clean your output folder before installing the Sentry plugin. File will not be source mapped: ${bundleFilePath}` - ); - return; - } - - const uniqueUploadName = `${debugId}-${chunkIndex}`; - - bundleContent = addDebugIdToBundleSource(bundleContent, debugId); - const writeSourceFilePromise = fs.promises.writeFile( - path.join(uploadFolder, `${uniqueUploadName}.js`), - bundleContent, - "utf-8" - ); - - const writeSourceMapFilePromise = determineSourceMapPathFromBundle( - bundleFilePath, - bundleContent, - logger, - resolveSourceMapHook - ).then(async (sourceMapPath) => { - if (sourceMapPath) { - await prepareSourceMapForDebugIdUpload( - sourceMapPath, - path.join(uploadFolder, `${uniqueUploadName}.js.map`), - debugId, - rewriteSourcesHook, - logger - ); - } - }); - - await writeSourceFilePromise; - await writeSourceMapFilePromise; -} - -/** - * Looks for a particular string pattern (`sdbid-[debug ID]`) in the bundle - * source and extracts the bundle's debug ID from it. - * - * The string pattern is injected via the debug ID injection snipped. - */ -function determineDebugIdFromBundleSource(code: string): string | undefined { - const match = code.match( - /sentry-dbid-([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/ - ); - - if (match) { - return match[1]; - } else { - return undefined; - } -} - -const SPEC_LAST_DEBUG_ID_REGEX = /\/\/# debugId=([a-fA-F0-9-]+)(?![\s\S]*\/\/# debugId=)/m; - -function hasSpecCompliantDebugId(bundleSource: string): boolean { - return SPEC_LAST_DEBUG_ID_REGEX.test(bundleSource); -} - -function addDebugIdToBundleSource(bundleSource: string, debugId: string): string { - if (hasSpecCompliantDebugId(bundleSource)) { - return bundleSource.replace(SPEC_LAST_DEBUG_ID_REGEX, `//# debugId=${debugId}`); - } else { - return `${bundleSource}\n//# debugId=${debugId}`; - } -} - -/** - * Applies a set of heuristics to find the source map for a particular bundle. - * - * @returns the path to the bundle's source map or `undefined` if none could be found. - */ -export async function determineSourceMapPathFromBundle( - bundlePath: string, - bundleSource: string, - logger: Logger, - resolveSourceMapHook: ResolveSourceMapHook | undefined -): Promise { - const sourceMappingUrlMatch = bundleSource.match(/^\s*\/\/# sourceMappingURL=(.*)$/m); - const sourceMappingUrl = sourceMappingUrlMatch ? (sourceMappingUrlMatch[1] as string) : undefined; - - const searchLocations: string[] = []; - - if (resolveSourceMapHook) { - logger.debug( - `Calling sourcemaps.resolveSourceMap(${JSON.stringify(bundlePath)}, ${JSON.stringify( - sourceMappingUrl - )})` - ); - const customPath = await resolveSourceMapHook(bundlePath, sourceMappingUrl); - logger.debug(`resolveSourceMap hook returned: ${JSON.stringify(customPath)}`); - - if (customPath) { - searchLocations.push(customPath); - } - } - - // 1. try to find source map at `sourceMappingURL` location - if (sourceMappingUrl) { - let parsedUrl: URL | undefined; - try { - parsedUrl = new URL(sourceMappingUrl); - } catch { - // noop - } - - if (parsedUrl?.protocol === "file:") { - searchLocations.push(url.fileURLToPath(sourceMappingUrl)); - } else if (parsedUrl) { - // noop, non-file urls don't translate to a local sourcemap file - } else if (path.isAbsolute(sourceMappingUrl)) { - searchLocations.push(path.normalize(sourceMappingUrl)); - } else { - searchLocations.push(path.normalize(path.join(path.dirname(bundlePath), sourceMappingUrl))); - } - } - - // 2. try to find source map at path adjacent to chunk source, but with `.map` appended - searchLocations.push(`${bundlePath}.map`); - - for (const searchLocation of searchLocations) { - try { - await util.promisify(fs.access)(searchLocation); - logger.debug(`Source map found for bundle \`${bundlePath}\`: \`${searchLocation}\``); - return searchLocation; - } catch { - // noop - } - } - - // This is just a debug message because it can be quite spammy for some frameworks - logger.debug( - `Could not determine source map path for bundle \`${bundlePath}\`` + - ` with sourceMappingURL=${ - sourceMappingUrl === undefined ? "undefined" : `\`${sourceMappingUrl}\`` - }` + - ` - Did you turn on source map generation in your bundler?` + - ` (Attempted paths: ${searchLocations.map((e) => `\`${e}\``).join(", ")})` - ); - return undefined; -} - -/** - * Reads a source map, injects debug ID fields, and writes the source map to the target path. - */ -async function prepareSourceMapForDebugIdUpload( - sourceMapPath: string, - targetPath: string, - debugId: string, - rewriteSourcesHook: RewriteSourcesHook, - logger: Logger -): Promise { - let sourceMapFileContent: string; - try { - sourceMapFileContent = await util.promisify(fs.readFile)(sourceMapPath, { - encoding: "utf8", - }); - } catch (e) { - logger.error(`Failed to read source map for debug ID upload: ${sourceMapPath}`, e); - return; - } - - let map: Record; - try { - map = JSON.parse(sourceMapFileContent) as { sources: unknown; [key: string]: unknown }; - // For now we write both fields until we know what will become the standard - if ever. - map["debug_id"] = debugId; - map["debugId"] = debugId; - } catch { - logger.error(`Failed to parse source map for debug ID upload: ${sourceMapPath}`); - return; - } - - if (map["sources"] && Array.isArray(map["sources"])) { - const mapDir = path.dirname(sourceMapPath); - map["sources"] = map["sources"].map((source: string) => - rewriteSourcesHook(source, map, { mapDir }) - ); - } - - try { - await util.promisify(fs.writeFile)(targetPath, JSON.stringify(map), { - encoding: "utf8", - }); - } catch (e) { - logger.error(`Failed to prepare source map for debug ID upload: ${sourceMapPath}`, e); - return; - } -} - -const PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//; -export function defaultRewriteSourcesHook(source: string): string { - if (source.match(PROTOCOL_REGEX)) { - return source.replace(PROTOCOL_REGEX, ""); - } else { - return path.relative(process.cwd(), path.normalize(source)); - } -} diff --git a/packages/bundler-plugins/src/core/glob.ts b/packages/bundler-plugins/src/core/glob.ts deleted file mode 100644 index b413e2c6..00000000 --- a/packages/bundler-plugins/src/core/glob.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { glob } from "glob"; - -export function globFiles( - patterns: string | string[], - options?: { root?: string; ignore?: string | string[] } -): Promise { - return glob(patterns, { absolute: true, nodir: true, ...options }); -} diff --git a/packages/bundler-plugins/src/core/index.ts b/packages/bundler-plugins/src/core/index.ts deleted file mode 100644 index 425c86f9..00000000 --- a/packages/bundler-plugins/src/core/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { transformAsync } from "@babel/core"; -import componentNameAnnotatePlugin, { - experimentalComponentNameAnnotatePlugin, -} from "../babel-plugin"; -import SentryCli from "@sentry/cli"; -import { debug } from "@sentry/core"; -import * as fs from "fs"; -import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from "./utils"; - -/** - * Determines whether the Sentry CLI binary is in its expected location. - * This function is useful since `@sentry/cli` installs the binary via a post-install - * script and post-install scripts may not always run. E.g. with `npm i --ignore-scripts`. - */ -export function sentryCliBinaryExists(): boolean { - return fs.existsSync(SentryCli.getPath()); -} - -// We need to be careful not to inject the snippet before any `"use strict";`s. -// As an additional complication `"use strict";`s may come after any number of comments. -export const COMMENT_USE_STRICT_REGEX = - // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. - /^(?:\s*|\/\*(?:.|\r|\n)*?\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/; - -/** - * Checks if a file is a JavaScript file based on its extension. - * Handles query strings and hashes in the filename. - */ -export function isJsFile(fileName: string): boolean { - const cleanFileName = stripQueryAndHashFromPath(fileName); - return [".js", ".mjs", ".cjs"].some((ext) => cleanFileName.endsWith(ext)); -} - -/** - * Checks if a chunk should be skipped for code injection - * - * This is necessary to handle Vite's MPA (multi-page application) mode where - * HTML entry points create "facade" chunks that should not contain injected code. - * See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829 - * - * However, in SPA mode, the main bundle also has an HTML facade but contains - * substantial application code. We should NOT skip injection for these bundles. - * - * @param code - The chunk's code content - * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks - * @returns true if the chunk should be skipped - */ -export function shouldSkipCodeInjection( - code: string, - facadeModuleId: string | null | undefined -): boolean { - // Skip empty chunks - these are placeholder chunks that should be optimized away - if (code.trim().length === 0) { - return true; - } - - // For HTML facade chunks, only skip if they contain only import statements - if (facadeModuleId && stripQueryAndHashFromPath(facadeModuleId).endsWith(".html")) { - return containsOnlyImports(code); - } - - return false; -} - -export { globFiles } from "./glob"; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function createComponentNameAnnotateHooks( - ignoredComponents: string[], - injectIntoHtml: boolean -) { - type ParserPlugins = NonNullable< - NonNullable[1]>["parserOpts"] - >["plugins"]; - - return { - async transform(this: void, code: string, id: string) { - // id may contain query and hash which will trip up our file extension logic below - const idWithoutQueryAndHash = stripQueryAndHashFromPath(id); - - if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) { - return null; - } - - // We will only apply this plugin on jsx and tsx files - if (![".jsx", ".tsx"].some((ending) => idWithoutQueryAndHash.endsWith(ending))) { - return null; - } - - const parserPlugins: ParserPlugins = []; - if (idWithoutQueryAndHash.endsWith(".jsx")) { - parserPlugins.push("jsx"); - } else if (idWithoutQueryAndHash.endsWith(".tsx")) { - parserPlugins.push("jsx", "typescript"); - } - - const plugin = injectIntoHtml - ? experimentalComponentNameAnnotatePlugin - : componentNameAnnotatePlugin; - - try { - const result = await transformAsync(code, { - plugins: [[plugin, { ignoredComponents }]], - filename: id, - parserOpts: { - sourceType: "module", - allowAwaitOutsideFunction: true, - plugins: parserPlugins, - }, - generatorOpts: { - decoratorsBeforeExport: true, - }, - sourceMaps: true, - }); - - return { - code: result?.code ?? code, - map: result?.map, - }; - } catch (e) { - debug.error(`Failed to apply react annotate plugin`, e); - } - - return { code }; - }, - }; -} - -export function getDebugIdSnippet(debugId: string): CodeInjection { - return new CodeInjection( - `var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}");` - ); -} - -export type { Logger } from "./logger"; -export type { Options, SentrySDKBuildFlags } from "./types"; -export { - CodeInjection, - replaceBooleanFlagsInCode, - stringToUUID, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, -} from "./utils"; -export { createSentryBuildPluginManager } from "./build-plugin-manager"; -export { createDebugIdUploadFunction } from "./debug-id-upload"; diff --git a/packages/bundler-plugins/src/core/logger.ts b/packages/bundler-plugins/src/core/logger.ts deleted file mode 100644 index 70528517..00000000 --- a/packages/bundler-plugins/src/core/logger.ts +++ /dev/null @@ -1,42 +0,0 @@ -interface LoggerOptions { - silent: boolean; - debug: boolean; - prefix: string; -} - -export type Logger = { - info(message: string, ...params: unknown[]): void; - warn(message: string, ...params: unknown[]): void; - error(message: string, ...params: unknown[]): void; - debug(message: string, ...params: unknown[]): void; -}; - -// Logging everything to stderr not to interfere with stdout -export function createLogger(options: LoggerOptions): Logger { - return { - info(message: string, ...params: unknown[]) { - if (!options.silent) { - // eslint-disable-next-line no-console - console.info(`${options.prefix} Info: ${message}`, ...params); - } - }, - warn(message: string, ...params: unknown[]) { - if (!options.silent) { - // eslint-disable-next-line no-console - console.warn(`${options.prefix} Warning: ${message}`, ...params); - } - }, - error(message: string, ...params: unknown[]) { - if (!options.silent) { - // eslint-disable-next-line no-console - console.error(`${options.prefix} Error: ${message}`, ...params); - } - }, - debug(message: string, ...params: unknown[]) { - if (!options.silent && options.debug) { - // eslint-disable-next-line no-console - console.debug(`${options.prefix} Debug: ${message}`, ...params); - } - }, - }; -} diff --git a/packages/bundler-plugins/src/core/options-mapping.ts b/packages/bundler-plugins/src/core/options-mapping.ts deleted file mode 100644 index e75093e1..00000000 --- a/packages/bundler-plugins/src/core/options-mapping.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { Logger } from "./logger"; -import type { - Options as UserOptions, - SetCommitsOptions, - RewriteSourcesHook, - ResolveSourceMapHook, - IncludeEntry, - ModuleMetadata, - ModuleMetadataCallback, -} from "./types"; -import { determineReleaseName } from "./utils"; - -export type NormalizedOptions = { - org: string | undefined; - project: string | string[] | undefined; - authToken: string | undefined; - url: string; - headers: Record | undefined; - debug: boolean; - silent: boolean; - errorHandler: ((err: Error) => void) | undefined; - telemetry: boolean; - disable: boolean; - sourcemaps: - | { - disable?: boolean | "disable-upload"; - assets?: string | string[]; - ignore?: string | string[]; - rewriteSources?: RewriteSourcesHook; - resolveSourceMap?: ResolveSourceMapHook; - filesToDeleteAfterUpload?: string | string[] | Promise; - } - | undefined; - release: { - name: string | undefined; - inject: boolean; - create: boolean; - finalize: boolean; - vcsRemote: string; - setCommits: - | (SetCommitsOptions & { - shouldNotThrowOnFailure?: boolean; - }) - | false - | undefined; - dist?: string; - deploy?: - | { - env: string; - started?: number | string; - finished?: number | string; - time?: number; - name?: string; - url?: string; - } - | false; - uploadLegacySourcemaps?: string | IncludeEntry | Array; - }; - bundleSizeOptimizations: - | { - excludeDebugStatements?: boolean; - excludeTracing?: boolean; - excludeReplayCanvas?: boolean; - excludeReplayShadowDom?: boolean; - excludeReplayIframe?: boolean; - excludeReplayWorker?: boolean; - } - | undefined; - reactComponentAnnotation: - | { - enabled?: boolean; - ignoredComponents?: string[]; - _experimentalInjectIntoHtml?: boolean; - } - | undefined; - _metaOptions: { - telemetry: { - metaFramework: string | undefined; - }; - }; - applicationKey: string | undefined; - moduleMetadata: ModuleMetadata | ModuleMetadataCallback | undefined; - _experiments: { - injectBuildInformation?: boolean; - } & Record; -}; - -export const SENTRY_SAAS_URL = "https://sentry.io"; - -// oxlint-disable-next-line complexity -export function normalizeUserOptions(userOptions: UserOptions): NormalizedOptions { - const options = { - org: userOptions.org ?? process.env["SENTRY_ORG"], - project: - userOptions.project ?? - (process.env["SENTRY_PROJECT"]?.includes(",") - ? process.env["SENTRY_PROJECT"].split(",").map((p) => p.trim()) - : process.env["SENTRY_PROJECT"]), - authToken: userOptions.authToken ?? process.env["SENTRY_AUTH_TOKEN"], - url: userOptions.url ?? process.env["SENTRY_URL"] ?? SENTRY_SAAS_URL, - headers: userOptions.headers, - debug: userOptions.debug ?? false, - silent: userOptions.silent ?? false, - errorHandler: userOptions.errorHandler, - telemetry: userOptions.telemetry ?? true, - disable: userOptions.disable ?? false, - sourcemaps: userOptions.sourcemaps, - release: { - ...userOptions.release, - name: userOptions.release?.name ?? process.env["SENTRY_RELEASE"] ?? determineReleaseName(), - inject: userOptions.release?.inject ?? true, - create: userOptions.release?.create ?? true, - finalize: userOptions.release?.finalize ?? true, - vcsRemote: userOptions.release?.vcsRemote ?? process.env["SENTRY_VSC_REMOTE"] ?? "origin", - setCommits: userOptions.release?.setCommits as - | (SetCommitsOptions & { shouldNotThrowOnFailure?: boolean }) - | false - | undefined, - }, - bundleSizeOptimizations: userOptions.bundleSizeOptimizations, - reactComponentAnnotation: userOptions.reactComponentAnnotation, - _metaOptions: { - telemetry: { - metaFramework: userOptions._metaOptions?.telemetry?.metaFramework, - bundlerMajorVersion: userOptions._metaOptions?.telemetry?.bundlerMajorVersion, - }, - }, - applicationKey: userOptions.applicationKey, - moduleMetadata: userOptions.moduleMetadata, - _experiments: userOptions._experiments ?? {}, - }; - - if (options.release.setCommits === undefined) { - if ( - process.env["VERCEL"] && - process.env["VERCEL_GIT_COMMIT_SHA"] && - process.env["VERCEL_GIT_REPO_SLUG"] && - process.env["VERCEL_GIT_REPO_OWNER"] && - // We only want to set commits for the production env because Sentry becomes extremely noisy (eg on slack) for - // preview environments because the previous commit is always the "stem" commit of the preview/PR causing Sentry - // to notify you for other people creating PRs. - process.env["VERCEL_TARGET_ENV"] === "production" - ) { - options.release.setCommits = { - shouldNotThrowOnFailure: true, - commit: process.env["VERCEL_GIT_COMMIT_SHA"], - previousCommit: process.env["VERCEL_GIT_PREVIOUS_SHA"], - repo: `${process.env["VERCEL_GIT_REPO_OWNER"]}/${process.env["VERCEL_GIT_REPO_SLUG"]}`, - ignoreEmpty: true, - ignoreMissing: true, - }; - } else { - options.release.setCommits = { - shouldNotThrowOnFailure: true, - auto: true, - ignoreEmpty: true, - ignoreMissing: true, - }; - } - } - - if ( - options.release.deploy === undefined && - process.env["VERCEL"] && - process.env["VERCEL_TARGET_ENV"] - ) { - options.release.deploy = { - env: `vercel-${process.env["VERCEL_TARGET_ENV"]}`, - url: process.env["VERCEL_URL"] ? `https://${process.env["VERCEL_URL"]}` : undefined, - }; - } - - return options; -} - -/** - * Validates a few combinations of options that are not checked by Sentry CLI. - * - * For all other options, we can rely on Sentry CLI to validate them. In fact, - * we can't validate them in the plugin because Sentry CLI might pick up options from - * its config file. - * - * @param options the internal options - * @param logger the logger - * - * @returns `true` if the options are valid, `false` otherwise - */ -export function validateOptions(options: NormalizedOptions, logger: Logger): boolean { - const setCommits = options.release?.setCommits; - if (setCommits) { - if (!setCommits.auto && !(setCommits.repo && setCommits.commit)) { - logger.error( - "The `setCommits` option was specified but is missing required properties.", - "Please set either `auto` or both, `repo` and `commit`." - ); - return false; - } - if (setCommits.auto && setCommits.repo && setCommits) { - logger.warn( - "The `setCommits` options includes `auto` but also `repo` and `commit`.", - "Ignoring `repo` and `commit`.", - "Please only set either `auto` or both, `repo` and `commit`." - ); - } - } - - if ( - options.release?.deploy && - typeof options.release.deploy === "object" && - !options.release.deploy.env - ) { - logger.error( - "The `deploy` option was specified but is missing the required `env` property.", - "Please set the `env` property." - ); - return false; - } - - if (options.project && Array.isArray(options.project)) { - if (options.project.length === 0) { - logger.error( - "The `project` option was specified as an array but is empty.", - "Please provide at least one project slug." - ); - return false; - } - // Check each project is a non-empty string - const invalidProjects = options.project.filter((p) => typeof p !== "string" || p.trim() === ""); - if (invalidProjects.length > 0) { - logger.error( - "The `project` option contains invalid project slugs.", - "All projects must be non-empty strings." - ); - return false; - } - } - - return true; -} diff --git a/packages/bundler-plugins/src/core/sentry/telemetry.ts b/packages/bundler-plugins/src/core/sentry/telemetry.ts deleted file mode 100644 index d0ed9836..00000000 --- a/packages/bundler-plugins/src/core/sentry/telemetry.ts +++ /dev/null @@ -1,181 +0,0 @@ -import SentryCli from "@sentry/cli"; -import type { Client } from "@sentry/types"; -import type { ServerRuntimeClientOptions } from "@sentry/core"; -import { applySdkMetadata, ServerRuntimeClient } from "@sentry/core"; -import type { NormalizedOptions } from "../options-mapping"; -import { SENTRY_SAAS_URL } from "../options-mapping"; -import { Scope } from "@sentry/core"; -import { createStackParser, nodeStackLineParser } from "@sentry/core"; -import { makeOptionallyEnabledNodeTransport } from "./transports"; -import { getProjects } from "../utils"; -import { LIB_VERSION } from "../version"; - -const SENTRY_SAAS_HOSTNAME = "sentry.io"; - -const stackParser = createStackParser(nodeStackLineParser()); - -export function createSentryInstance( - options: NormalizedOptions, - shouldSendTelemetry: Promise, - buildTool: string, - buildToolMajorVersion: string | undefined -): { sentryScope: Scope; sentryClient: Client } { - const clientOptions: ServerRuntimeClientOptions = { - platform: "node", - runtime: { name: "node", version: global.process.version }, - - dsn: "https://4c2bae7d9fbc413e8f7385f55c515d51@o1.ingest.sentry.io/6690737", - - tracesSampleRate: 1, - sampleRate: 1, - - release: LIB_VERSION, - integrations: [], - tracePropagationTargets: ["sentry.io/api"], - - stackParser, - - beforeSend: (event) => { - event.exception?.values?.forEach((exception) => { - delete exception.stacktrace; - }); - - delete event.server_name; // Server name might contain PII - return event; - }, - - beforeSendTransaction: (event) => { - delete event.server_name; // Server name might contain PII - return event; - }, - - // We create a transport that stalls sending events until we know that we're allowed to (i.e. when Sentry CLI told - // us that the upload URL is the Sentry SaaS URL) - transport: makeOptionallyEnabledNodeTransport(shouldSendTelemetry), - }; - - applySdkMetadata(clientOptions, "node"); - - const client = new ServerRuntimeClient(clientOptions); - const scope = new Scope(); - scope.setClient(client); - - setTelemetryDataOnScope(options, scope, buildTool, buildToolMajorVersion); - - return { sentryScope: scope, sentryClient: client }; -} - -export function setTelemetryDataOnScope( - options: NormalizedOptions, - scope: Scope, - buildTool: string, - buildToolMajorVersion?: string -): void { - const { org, project, release, errorHandler, sourcemaps, reactComponentAnnotation } = options; - - scope.setTag("upload-legacy-sourcemaps", !!release.uploadLegacySourcemaps); - if (release.uploadLegacySourcemaps) { - scope.setTag( - "uploadLegacySourcemapsEntries", - Array.isArray(release.uploadLegacySourcemaps) ? release.uploadLegacySourcemaps.length : 1 - ); - } - - scope.setTag("module-metadata", !!options.moduleMetadata); - scope.setTag("inject-build-information", !!options._experiments.injectBuildInformation); - - // Optional release pipeline steps - if (release.setCommits) { - scope.setTag("set-commits", release.setCommits.auto === true ? "auto" : "manual"); - } else { - scope.setTag("set-commits", "undefined"); - } - scope.setTag("finalize-release", release.finalize); - scope.setTag("deploy-options", !!release.deploy); - - // Miscellaneous options - scope.setTag("custom-error-handler", !!errorHandler); - scope.setTag("sourcemaps-assets", !!sourcemaps?.assets); - scope.setTag("delete-after-upload", !!sourcemaps?.filesToDeleteAfterUpload); - scope.setTag("sourcemaps-disabled", !!sourcemaps?.disable); - - scope.setTag("react-annotate", !!reactComponentAnnotation?.enabled); - - scope.setTag("node", process.version); - scope.setTag("platform", process.platform); - - scope.setTag("meta-framework", options._metaOptions.telemetry.metaFramework ?? "none"); - - scope.setTag("application-key-set", options.applicationKey !== undefined); - - scope.setTag("ci", !!process.env["CI"]); - - scope.setTags({ - organization: org, - project: Array.isArray(project) ? project.join(", ") : (project ?? "undefined"), - bundler: buildTool, - }); - - if (buildToolMajorVersion) { - scope.setTag("bundler-major-version", buildToolMajorVersion); - } - - scope.setUser({ id: org }); -} - -export async function allowedToSendTelemetry(options: NormalizedOptions): Promise { - const { silent, org, project, authToken, url, headers, telemetry, release } = options; - - // `options.telemetry` defaults to true - if (telemetry === false) { - return false; - } - - if (url === SENTRY_SAAS_URL) { - return true; - } - - const cli = new SentryCli(null, { - url, - authToken, - org, - project: getProjects(project)?.[0], - vcsRemote: release.vcsRemote, - silent, - headers, - }); - - let cliInfo; - try { - // Makes a call to SentryCLI to get the Sentry server URL the CLI uses. - // We need to check and decide to use telemetry based on the CLI's response to this call - // because only at this time we checked a possibly existing .sentryclirc file. This file - // could point to another URL than the default URL. - cliInfo = await cli.execute(["info"], false); - } catch { - return false; - } - - const cliInfoUrl = cliInfo - .split(/(\r\n|\n|\r)/)[0] - ?.replace(/^Sentry Server: /, "") - ?.trim(); - - if (cliInfoUrl === undefined) { - return false; - } - - return new URL(cliInfoUrl).hostname === SENTRY_SAAS_HOSTNAME; -} - -/** - * Flushing the SDK client can fail. We never want to crash the plugin because of telemetry. - */ -export async function safeFlushTelemetry(sentryClient: Client): Promise { - try { - await sentryClient.flush(2000); - } catch { - // Noop when flushing fails. - // We don't even need to log anything because there's likely nothing the user can do and they likely will not care. - } -} diff --git a/packages/bundler-plugins/src/core/sentry/transports.ts b/packages/bundler-plugins/src/core/sentry/transports.ts deleted file mode 100644 index 0626e3b1..00000000 --- a/packages/bundler-plugins/src/core/sentry/transports.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * This is a simplified version of the Sentry Node SDK's HTTP transport. - */ -import * as https from "node:https"; -import { Readable } from "node:stream"; -import { createGzip } from "node:zlib"; -import { createTransport, suppressTracing } from "@sentry/core"; -import type { - BaseTransportOptions, - Transport, - TransportMakeRequestResponse, - TransportRequest, - TransportRequestExecutor, -} from "@sentry/types"; -import { join } from "node:path"; -import { appendFileSync, mkdirSync } from "node:fs"; - -// Estimated maximum size for reasonable standalone event -const GZIP_THRESHOLD = 1024 * 32; - -/** - * Gets a stream from a Uint8Array or string - * Readable.from is ideal but was added in node.js v12.3.0 and v10.17.0 - */ -function streamFromBody(body: Uint8Array | string): Readable { - return new Readable({ - read() { - this.push(body); - this.push(null); - }, - }); -} - -/** - * Creates a RequestExecutor to be used with `createTransport`. - */ -function createRequestExecutor(options: BaseTransportOptions): TransportRequestExecutor { - const { hostname, pathname, port, protocol, search } = new URL(options.url); - return function makeRequest(request: TransportRequest): Promise { - return new Promise((resolve, reject) => { - suppressTracing(() => { - let body = streamFromBody(request.body); - - const headers: Record = {}; - - if (request.body.length > GZIP_THRESHOLD) { - headers["content-encoding"] = "gzip"; - body = body.pipe(createGzip()); - } - - const req = https.request( - { - method: "POST", - headers, - hostname, - path: `${pathname}${search}`, - port, - protocol, - }, - (res) => { - res.on("data", () => { - // Drain socket - }); - - res.on("end", () => { - // Drain socket - }); - - res.setEncoding("utf8"); - - // "Key-value pairs of header names and values. Header names are lower-cased." - // https://nodejs.org/api/http.html#http_message_headers - const retryAfterHeader = res.headers["retry-after"] ?? null; - const rateLimitsHeader = res.headers["x-sentry-rate-limits"] ?? null; - - resolve({ - statusCode: res.statusCode, - headers: { - "retry-after": retryAfterHeader, - "x-sentry-rate-limits": Array.isArray(rateLimitsHeader) - ? rateLimitsHeader[0] || null - : rateLimitsHeader, - }, - }); - } - ); - - req.on("error", reject); - body.pipe(req); - }); - }); - }; -} - -/** - * Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry. - */ -function makeNodeTransport(options: BaseTransportOptions): Transport { - const requestExecutor = createRequestExecutor(options); - return createTransport(options, requestExecutor); -} - -/** A transport that can be optionally enabled as a later time than it's - * creation */ -export function makeOptionallyEnabledNodeTransport( - shouldSendTelemetry: Promise -): (options: BaseTransportOptions) => Transport { - return (nodeTransportOptions) => { - const nodeTransport = makeNodeTransport(nodeTransportOptions); - - return { - flush: (timeout) => nodeTransport.flush(timeout), - send: async (request) => { - // If global.__SENTRY_INTERCEPT_TRANSPORT__ is an array, we push the - // envelope into it for testing purposes. - if ( - "__SENTRY_INTERCEPT_TRANSPORT__" in global && - Array.isArray(global.__SENTRY_INTERCEPT_TRANSPORT__) - ) { - global.__SENTRY_INTERCEPT_TRANSPORT__.push(request); - return { statusCode: 200 }; - } - - if (await shouldSendTelemetry) { - if (process.env["SENTRY_TEST_OUT_DIR"]) { - const outDir = process.env["SENTRY_TEST_OUT_DIR"]; - mkdirSync(outDir, { recursive: true }); - const path = join(outDir, "sentry-telemetry.json"); - appendFileSync(path, `${JSON.stringify(request)},\n`); - return { statusCode: 200 }; - } - - return nodeTransport.send(request); - } - - return { statusCode: 200 }; - }, - }; - }; -} diff --git a/packages/bundler-plugins/src/core/types.ts b/packages/bundler-plugins/src/core/types.ts deleted file mode 100644 index 2d197e3b..00000000 --- a/packages/bundler-plugins/src/core/types.ts +++ /dev/null @@ -1,640 +0,0 @@ -export interface Options { - /** - * The slug of the Sentry organization associated with the app. - * - * This value can also be specified via the `SENTRY_ORG` environment variable. - */ - org?: string; - - /** - * The slug of the Sentry project associated with the app. - * - * When uploading source maps, you can specify multiple projects (as an array) to upload - * the same source maps to multiple projects. This is useful in monorepo environments - * where multiple projects share the same release. - * - * This value can also be specified via the `SENTRY_PROJECT` environment variable. - */ - project?: string | string[]; - - /** - * The authentication token to use for all communication with Sentry. - * Can be obtained from https://sentry.io/orgredirect/organizations/:orgslug/settings/auth-tokens/. - * - * This value can also be specified via the `SENTRY_AUTH_TOKEN` environment variable. - * - * @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens - */ - authToken?: string | undefined; - - /** - * The base URL of your Sentry instance. Use this if you are using a self-hosted - * or Sentry instance other than sentry.io. - * - * This value can also be set via the `SENTRY_URL` environment variable. - * - * @default "https://sentry.io" (correct value for SaaS customers) - */ - url?: string; - - /** - * Additional headers to send with every outgoing request to Sentry. - */ - headers?: Record; - - /** - * Enable debug information logs during build-time. - * Enabling this will give you, for example, logs about source maps. - * - * This option also propagates the debug flag to the Sentry CLI by setting - * the `SENTRY_LOG_LEVEL` environment variable to `"debug"` if it's not already set. - * If you have explicitly set `SENTRY_LOG_LEVEL`, this option will be ignored. - * - * @default false - */ - debug?: boolean; - - /** - * Suppresses all build logs (all log levels, including errors). - * - * @default false - */ - silent?: boolean; - - /** - * When an error occurs during release creation or sourcemaps upload, the plugin will call this function. - * - * By default, the plugin will simply throw an error, thereby stopping the bundling process. - * If an `errorHandler` callback is provided, compilation will continue, unless an error is - * thrown in the provided callback. - * - * To allow compilation to continue but still emit a warning, set this option to the following: - * - * ```js - * (err) => { - * console.warn(err); - * } - * ``` - */ - errorHandler?: (err: Error) => void; - - /** - * If this flag is `true`, internal plugin errors and performance data will be sent to Sentry. - * It will not collect any sensitive or user-specific data. - * - * At Sentry, we like to use Sentry ourselves to deliver faster and more stable products. - * We're very careful of what we're sending. We won't collect anything other than error - * and high-level performance data. We will never collect your code or any details of the - * projects in which you're using this plugin. - * - * @default true - */ - telemetry?: boolean; - - /** - * Completely disables all functionality of the plugin. - * - * Defaults to `false`. - */ - disable?: boolean; - - /** - * Options related to source maps upload and processing. - */ - sourcemaps?: { - /** - * Disables all functionality related to sourcemaps if set to `true`. - * - * If set to `"disable-upload"`, the plugin will not upload sourcemaps to Sentry, but will inject debug IDs into the build artifacts. - * This is useful if you want to manually upload sourcemaps to Sentry at a later point in time. - * - * @default false - */ - disable?: boolean | "disable-upload"; - - /** - * A glob or an array of globs that specify the build artifacts and source maps that will be uploaded to Sentry. - * - * The globbing patterns must follow the implementation of the `glob` package: https://www.npmjs.com/package/glob#glob-primer - * - * If this option is not specified, the plugin will try to upload all JavaScript files and source map files that are created during build. - * - * Use the `debug` option to print information about which files end up being uploaded. - * - */ - assets?: string | string[]; - - /** - * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. - * - * The globbing patterns must follow the implementation of the `glob` package: https://www.npmjs.com/package/glob#glob-primer - * - * Use the `debug` option to print information about which files end up being uploaded. - * - * @default [] - */ - ignore?: string | string[]; - - /** - * Hook to rewrite the `sources` field inside the source map before being uploaded to Sentry. Does not modify the actual source map. - * - * The hook receives the source path, the parsed source map object, and a context object containing `mapDir` - - * the directory of the source map file, useful for resolving relative source paths. - * - * Defaults to making all sources relative to `process.cwd()` while building. - */ - rewriteSources?: RewriteSourcesHook; - - /** - * Hook to customize source map file resolution. - * - * The hook is called with the absolute path of the build artifact and the value of the `//# sourceMappingURL=` - * comment, if present. The hook should then return an absolute path (or a promise that resolves to one) indicating - * where to find the artifact's corresponding source map file. If no path is returned or the returned path doesn't - * exist, the standard source map resolution process will be used. - * - * The standard process first tries to resolve based on the `//# sourceMappingURL=` value (it supports `file://` - * urls and absolute/relative paths). If that path doesn't exist, it then looks for a file named - * `${artifactName}.map` in the same directory as the artifact. - * - * Note: This is mostly helpful for complex builds with custom source map generation. For example, if you put source - * maps into a separate directory and rewrite the `//# sourceMappingURL=` comment to something other than a relative - * directory, sentry will be unable to locate the source maps for a given build artifact. This hook allows you to - * implement the resolution process yourself. - * - * Use the `debug` option to print information about source map resolution. - */ - resolveSourceMap?: ResolveSourceMapHook; - - /** - * A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact upload to Sentry has been completed. - * - * Note: If you pass in a Promise that resolves to a string or array, the plugin will await the Promise and use - * the resolved value globs. This is useful if you need to dynamically determine the files to delete. Some - * higher-level Sentry SDKs or options use this feature (e.g., SvelteKit). - * - * The globbing patterns must follow the implementation of the `glob` package: https://www.npmjs.com/package/glob#glob-primer - * - * Use the `debug` option to print information about which files end up being deleted. - */ - filesToDeleteAfterUpload?: string | string[] | Promise; - }; - - /** - * Options related to managing the Sentry releases for a build. - * - * More info: https://docs.sentry.io/product/releases/ - */ - release?: { - /** - * Unique identifier for the release you want to create. - * - * This value can also be specified via the `SENTRY_RELEASE` environment variable. - * - * Defaults to automatically detecting a value for your environment. - * This includes values for Cordova, Heroku, AWS CodeBuild, CircleCI, Xcode, and Gradle, and otherwise uses the git `HEAD`'s commit SHA - * (the latter requires access to git CLI and for the root directory to be a valid repository). - * - * If no `name` is provided and the plugin can't automatically detect one, no release will be created. - */ - name?: string; - - /** - * Whether the plugin should inject release information into the build for the SDK to pick it up when sending events. (recommended) - * - * Defaults to `true`. - */ - inject?: boolean; - - /** - * Whether the plugin should create a release on Sentry during the build. - * - * Note that a release may still appear in Sentry even if this value is `false`. Any Sentry event that has a release value attached - * will automatically create a release (for example, via the `inject` option). - * - * @default true - */ - create?: boolean; - - /** - * Whether to automatically finalize the release. The release is finalized by adding an end timestamp after the build ends. - * - * @default true - */ - finalize?: boolean; - - /** - * Unique distribution identifier for the release. Used to further segment the release. - * - * Usually your build number. - */ - dist?: string; - - /** - * Version control system (VCS) remote name. - * - * This value can also be specified via the `SENTRY_VSC_REMOTE` environment variable. - * - * @default "origin" - */ - vcsRemote?: string; - - /** - * Configuration for associating the release with its commits in Sentry. - * - * Set to `false` to disable commit association. - * - * @default { auto: true } - */ - setCommits?: SetCommitsOptions | false; - - /** - * Configuration for adding deployment information to the release in Sentry. - * - * Set to `false` to disable automatic deployment detection and creation. - */ - deploy?: DeployOptions | false; - - /** - * Legacy method of uploading source maps. (not recommended unless necessary) - * - * One or more paths that should be scanned recursively for sources. - * - * Each path can be given as a string or an object with more specific options. - * - * The modern version of doing source maps upload is more robust and way easier to get working but has to inject a very small snippet of JavaScript into your output bundles. - * In situations where this leads to problems (e.g subresource integrity) you can use this option as a fallback. - */ - uploadLegacySourcemaps?: string | IncludeEntry | Array; - }; - - /** - * Options for bundle size optimizations by excluding certain features. - */ - bundleSizeOptimizations?: { - /** - * Exclude debug statements from the bundle, thus disabling features like the SDK's `debug` option. - * - * If set to `true`, the plugin will attempt to tree-shake (remove) any debugging code within the Sentry SDK during the build. - * Note that the success of this depends on tree-shaking being enabled in your build tooling. - * - * @default false - */ - excludeDebugStatements?: boolean; - - /** - * Exclude tracing functionality from the bundle, thus disabling features like performance monitoring. - * - * If set to `true`, the plugin will attempt to tree-shake (remove) code within the Sentry SDK that is related to tracing and performance monitoring. - * Note that the success of this depends on tree-shaking being enabled in your build tooling. - * - * **Notice:** Do not enable this when you're using any performance monitoring-related SDK features (e.g. `Sentry.startTransaction()`). - - * @default false - */ - excludeTracing?: boolean; - - /** - * If set to `true`, the plugin will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay Canvas recording functionality. - * Note that the success of this depends on tree-shaking being enabled in your build tooling. - * - * You can safely do this when you do not want to capture any Canvas activity via Sentry Session Replay. - * - * @deprecated In versions v7.78.0 and later of the Sentry JavaScript SDKs, canvas recording is opt-in making this option redundant. - */ - excludeReplayCanvas?: boolean; - - /** - * Exclude Replay Shadow DOM functionality from the bundle. - * - * If set to `true`, the plugin will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay Shadow DOM recording functionality. - * Note that the success of this depends on tree-shaking being enabled in your build tooling. - * - * This option is safe to be used when you do not want to capture any Shadow DOM activity via Sentry Session Replay. - * - * @default false - */ - excludeReplayShadowDom?: boolean; - - /** - * Exclude Replay iFrame functionality from the bundle. - * - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay `iframe` recording functionality. - * Note that the success of this depends on tree-shaking being enabled in your build tooling. - * - * You can safely do this when you do not want to capture any `iframe` activity via Sentry Session Replay. - * - * @default false - */ - excludeReplayIframe?: boolean; - - /** - * Exclude Replay worker functionality from the bundle. - * - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay's Compression Web Worker. - * Note that the success of this depends on tree-shaking being enabled in your build tooling. - * - * **Notice:** You should only use this option if you manually host a compression worker and configure it in your Sentry Session Replay integration config via the `workerUrl` option. - * - * @default false - */ - excludeReplayWorker?: boolean; - }; - - /** - * Options related to react component name annotations. - * Disabled by default, unless a value is set for this option. - * When enabled, your app's DOM will automatically be annotated during build-time with their respective component names. - * This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring. - * Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components - */ - reactComponentAnnotation?: { - /** - * 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[]; - /** - * An experimental component annotation injection mode that injects - * annotations into HTML rather than React components. - */ - _experimentalInjectIntoHtml?: boolean; - }; - - /** - * Metadata that should be associated with the built application. - * - * The metadata is serialized and can be looked up at runtime from within the SDK (for example in the `beforeSend`, - * event processors, or the transport), allowing for custom event filtering logic or routing of events. - * - * Metadata can either be passed directly or alternatively a callback can be provided that will be - * called with the following parameters: - * - `org`: The organization slug. - * - `project`: The project slug (when multiple projects are configured, this is the first project). - * - `projects`: An array of all project slugs (available when multiple projects are configured). - * - `release`: The release name. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleMetadata?: ModuleMetadata | ModuleMetadataCallback; - - /** - * A key which will embedded in all the bundled files. The SDK will be able to use the key to apply filtering - * rules, for example using the `thirdPartyErrorFilterIntegration`. - */ - applicationKey?: string; - - /** - * Options that are considered experimental and subject to change. - * - * @experimental API that does not follow semantic versioning and may change in any release - */ - _experiments?: { - /** - * If set to true, the plugin will inject an additional `SENTRY_BUILD_INFO` variable. - * This contains information about the build, e.g. dependencies, node version and other useful data. - * - * Defaults to `false`. - */ - injectBuildInformation?: boolean; - } & Record; - - /** - * Options that are useful for building wrappers around the plugin. You likely don't need these options unless you - * are distributing a tool that depends on this plugin - */ - _metaOptions?: { - /** - * Overrides the prefix that come before logger messages. (e.g. `[some-prefix] Info: Some log message`) - * - * Example value: `[sentry-webpack-plugin (client)]` - */ - loggerPrefixOverride?: string; - - /** - * Arbitrary telemetry items. - */ - telemetry?: { - /** - * The meta framework using the plugin. - */ - metaFramework?: string; - /** - * The major version of the bundler (e.g., "4" or "5" for webpack). - */ - bundlerMajorVersion?: string; - }; - }; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type RewriteSourcesHook = (source: string, map: any, context?: { mapDir: string }) => string; - -export type ResolveSourceMapHook = ( - artifactPath: string, - sourceMappingUrl: string | undefined -) => string | undefined | Promise; - -export interface ModuleMetadata { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export interface ModuleMetadataCallbackArgs { - org?: string; - project?: string; - projects?: string[]; - release?: string; -} - -export type ModuleMetadataCallback = (args: ModuleMetadataCallbackArgs) => ModuleMetadata; - -export type IncludeEntry = { - /** - * One or more paths to scan for files to upload. - */ - paths: string[]; - - /** - * One or more paths to ignore during upload. - * Overrides entries in ignoreFile file. - * - * Defaults to `['node_modules']` if neither `ignoreFile` nor `ignore` is set. - */ - ignore?: string | string[]; - - /** - * Path to a file containing list of files/directories to ignore. - * - * Can point to `.gitignore` or anything with the same format. - */ - ignoreFile?: string; - - /** - * Array of file extensions of files to be collected for the file upload. - * - * By default the following file extensions are processed: js, map, jsbundle and bundle. - */ - ext?: string[]; - - /** - * URL prefix to add to the beginning of all filenames. - * Defaults to '~/' but you might want to set this to the full URL. - * - * This is also useful if your files are stored in a sub folder. eg: url-prefix '~/static/js'. - */ - urlPrefix?: string; - - /** - * URL suffix to add to the end of all filenames. - * Useful for appending query parameters. - */ - urlSuffix?: string; - - /** - * When paired with the `rewrite` option, this will remove a prefix from filename references inside of - * sourcemaps. For instance you can use this to remove a path that is build machine specific. - * Note that this will NOT change the names of uploaded files. - */ - stripPrefix?: string[]; - - /** - * When paired with the `rewrite` option, this will add `~` to the `stripPrefix` array. - * - * Defaults to `false`. - */ - stripCommonPrefix?: boolean; - - /** - * Determines whether sentry-cli should attempt to link minified files with their corresponding maps. - * By default, it will match files and maps based on name, and add a Sourcemap header to each minified file - * for which it finds a map. Can be disabled if all minified files contain sourceMappingURL. - * - * Defaults to true. - */ - sourceMapReference?: boolean; - - /** - * Enables rewriting of matching source maps so that indexed maps are flattened and missing sources - * are inlined if possible. - * - * Defaults to true - */ - rewrite?: boolean; - - /** - * When `true`, attempts source map validation before upload if rewriting is not enabled. - * It will spot a variety of issues with source maps and cancel the upload if any are found. - * - * Defaults to `false` as this can cause false positives. - */ - validate?: boolean; -}; - -export interface SentrySDKBuildFlags extends Record { - __SENTRY_DEBUG__?: boolean; - __SENTRY_TRACING__?: boolean; - __RRWEB_EXCLUDE_CANVAS__?: boolean; - __RRWEB_EXCLUDE_IFRAME__?: boolean; - __RRWEB_EXCLUDE_SHADOW_DOM__?: boolean; - __SENTRY_EXCLUDE_REPLAY_WORKER__?: boolean; -} - -export type SetCommitsOptions = (AutoSetCommitsOptions | ManualSetCommitsOptions) & { - /** - * The commit before the beginning of this release (in other words, - * the last commit of the previous release). - * - * Defaults to the last commit of the previous release in Sentry. - * - * If there was no previous release, the last 10 commits will be used. - */ - previousCommit?: string; - - /** - * If the flag is to `true` and the previous release commit was not found - * in the repository, the plugin creates a release with the default commits - * count instead of failing the command. - * - * Defaults to `false`. - */ - ignoreMissing?: boolean; - - /** - * If this flag is set, the setCommits step will not fail and just exit - * silently if no new commits for a given release have been found. - * - * Defaults to `false`. - */ - ignoreEmpty?: boolean; -}; - -type AutoSetCommitsOptions = { - /** - * Automatically sets `commit` and `previousCommit`. Sets `commit` to `HEAD` - * and `previousCommit` as described in the option's documentation. - * - * If you set this to `true`, manually specified `commit` and `previousCommit` - * options will be overridden. It is best to not specify them at all if you - * set this option to `true`. - */ - auto: true; - - repo?: undefined; - commit?: undefined; -}; - -type ManualSetCommitsOptions = { - auto?: false | undefined; - - /** - * The full repo name as defined in Sentry. - * - * Required if the `auto` option is not set to `true`. - */ - repo: string; - - /** - * The current (last) commit in the release. - * - * Required if the `auto` option is not set to `true`. - */ - commit: string; -}; - -type DeployOptions = { - /** - * Environment for this release. Values that make sense here would - * be `production` or `staging`. - */ - env: string; - - /** - * Deployment start time in Unix timestamp (in seconds) or ISO 8601 format. - */ - started?: number | string; - - /** - * Deployment finish time in Unix timestamp (in seconds) or ISO 8601 format. - */ - finished?: number | string; - - /** - * Deployment duration (in seconds). Can be used instead of started and finished. - */ - time?: number; - - /** - * Human-readable name for the deployment. - */ - name?: string; - - /** - * URL that points to the deployment. - */ - url?: string; -}; - -export type HandleRecoverableErrorFn = (error: unknown, throwByDefault: boolean) => void; diff --git a/packages/bundler-plugins/src/core/utils.ts b/packages/bundler-plugins/src/core/utils.ts deleted file mode 100644 index 6ddb8cec..00000000 --- a/packages/bundler-plugins/src/core/utils.ts +++ /dev/null @@ -1,491 +0,0 @@ -/* oxlint-disable max-lines */ -import findUp from "find-up"; -import path from "path"; -import fs from "fs"; -import os from "os"; -import crypto from "crypto"; -import childProcess from "child_process"; -import type { SourceMap } from "magic-string"; -import MagicString from "magic-string"; - -/** - * Checks whether the given input is already an array, and if it isn't, wraps it in one. - * - * @param maybeArray Input to turn into an array, if necessary - * @returns The input, if already an array, or an array with the input as the only element, if not - */ -export function arrayify(maybeArray: T | T[]): T[] { - return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; -} - -type PackageJson = Record; - -/** - * Get the closes package.json from a given starting point upwards. - * This handles a few edge cases: - * * Check if a given file package.json appears to be an actual NPM package.json file - * * Stop at the home dir, to avoid looking too deeply - */ -export function getPackageJson({ cwd, stopAt }: { cwd?: string; stopAt?: string } = {}): - | PackageJson - | undefined { - return lookupPackageJson(cwd ?? process.cwd(), path.normalize(stopAt ?? os.homedir())); -} - -export function parseMajorVersion(ver: string): number | undefined { - let version = ver; - // if it has a `v` prefix, remove it - if (version.startsWith("v")) { - version = version.slice(1); - } - - // First, try simple lookup of exact, ~ and ^ versions - const regex = /^[\^~]?(\d+)(\.\d+)?(\.\d+)?(-.+)?/; - - const match = version.match(regex); - if (match) { - return parseInt(match[1] as string, 10); - } - - // Try to parse e.g. 1.x - const coerced = parseInt(version, 10); - if (!Number.isNaN(coerced)) { - return coerced; - } - - // Match <= and >= ranges. - const gteLteRegex = /^[<>]=\s*(\d+)(\.\d+)?(\.\d+)?(-.+)?/; - const gteLteMatch = version.match(gteLteRegex); - if (gteLteMatch) { - return parseInt(gteLteMatch[1] as string, 10); - } - - // match < ranges - const ltRegex = /^<\s*(\d+)(\.\d+)?(\.\d+)?(-.+)?/; - const ltMatch = version.match(ltRegex); - if (ltMatch) { - // Two scenarios: - // a) < 2.0.0 --> return 1 - // b) < 2.1.0 --> return 2 - - const major = parseInt(ltMatch[1] as string, 10); - - if ( - // minor version > 0 - (typeof ltMatch[2] === "string" && parseInt(ltMatch[2].slice(1), 10) > 0) || - // patch version > 0 - (typeof ltMatch[3] === "string" && parseInt(ltMatch[3].slice(1), 10) > 0) - ) { - return major; - } - - return major - 1; - } - - // match > ranges - const gtRegex = /^>\s*(\d+)(\.\d+)?(\.\d+)?(-.+)?/; - const gtMatch = version.match(gtRegex); - if (gtMatch) { - // We always return the version here, even though it _may_ be incorrect - // E.g. if given > 2.0.0, it should be 2 if there exists any 2.x.x version, else 3 - // Since there is no way for us to know this, we're going to assume any kind of patch/feature release probably exists - return parseInt(gtMatch[1] as string, 10); - } - return undefined; -} - -// This is an explicit list of packages where we want to include the (major) version number. -const PACKAGES_TO_INCLUDE_VERSION = [ - "react", - "@angular/core", - "vue", - "ember-source", - "svelte", - "@sveltejs/kit", - "webpack", - "vite", - "gatsby", - "next", - "remix", - "rollup", - "esbuild", -]; - -export function getDependencies(packageJson: PackageJson): { - deps: string[]; - depsVersions: Record; -} { - const dependencies: Record = Object.assign( - {}, - packageJson["devDependencies"] ?? {}, - packageJson["dependencies"] ?? {} - ); - - const deps = Object.keys(dependencies).sort(); - - const depsVersions: Record = deps.reduce( - (depsVersions, depName) => { - if (PACKAGES_TO_INCLUDE_VERSION.includes(depName)) { - const version = dependencies[depName] as string; - const majorVersion = parseMajorVersion(version); - if (majorVersion) { - depsVersions[depName] = majorVersion; - } - } - return depsVersions; - }, - {} as Record - ); - - return { deps, depsVersions }; -} - -function lookupPackageJson(cwd: string, stopAt: string): PackageJson | undefined { - const jsonPath = findUp.sync( - (dirName) => { - // Stop if we reach this dir - if (path.normalize(dirName) === stopAt) { - return findUp.stop; - } - - return findUp.sync.exists(`${dirName}/package.json`) ? "package.json" : undefined; - }, - { cwd } - ); - - if (!jsonPath) { - return undefined; - } - - try { - const jsonStr = fs.readFileSync(jsonPath, "utf8"); - const json = JSON.parse(jsonStr) as PackageJson; - - // Ensure it is an actual package.json - // This is very much not bulletproof, but should be good enough - if ("name" in json || "private" in json) { - return json; - } - } catch { - // Ignore and walk up - } - - // Continue up the tree, if we find a fitting package.json - const newCwd = path.dirname(path.resolve(`${jsonPath}/..`)); - return lookupPackageJson(newCwd, stopAt); -} - -/** - * Deterministically hashes a string and turns the hash into a uuid. - */ -export function stringToUUID(str: string): string { - const sha256Hash = crypto.createHash("sha256").update(str).digest("hex"); - - // Position 16 is fixed to either 8, 9, a, or b in the uuid v4 spec (10xx in binary) - // RFC 4122 section 4.4 - const v4variant = ["8", "9", "a", "b"][sha256Hash.substring(16, 17).charCodeAt(0) % 4] as string; - - return `${sha256Hash.substring(0, 8)}-${sha256Hash.substring(8, 12)}-4${sha256Hash.substring(13, 16)}-${v4variant}${sha256Hash.substring(17, 20)}-${sha256Hash.substring(20, 32)}`.toLowerCase(); -} - -function gitRevision(): string | undefined { - let gitRevision: string | undefined; - try { - gitRevision = childProcess - .execSync("git rev-parse HEAD", { stdio: ["ignore", "pipe", "ignore"], windowsHide: true }) - .toString() - .trim(); - } catch { - // noop - } - return gitRevision; -} - -/** - * Tries to guess a release name based on environmental data. - */ -// oxlint-disable-next-line complexity -export function determineReleaseName(): string | undefined { - // This list is in approximate alpha order, separated into 3 categories: - // 1. Git providers - // 2. CI providers with specific environment variables (has the provider name in the variable name) - // 3. CI providers with generic environment variables (checked for last to prevent possible false positives) - - const possibleReleaseNameOfGitProvider = - // GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables - process.env["GITHUB_SHA"] || - // GitLab CI - https://docs.gitlab.com/ee/ci/variables/predefined_variables.html - process.env["CI_MERGE_REQUEST_SOURCE_BRANCH_SHA"] || - process.env["CI_BUILD_REF"] || - process.env["CI_COMMIT_SHA"] || - // Bitbucket - https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/ - process.env["BITBUCKET_COMMIT"]; - - const possibleReleaseNameOfCiProvidersWithSpecificEnvVar = - // AppVeyor - https://www.appveyor.com/docs/environment-variables/ - process.env["APPVEYOR_PULL_REQUEST_HEAD_COMMIT"] || - process.env["APPVEYOR_REPO_COMMIT"] || - // AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html - process.env["CODEBUILD_RESOLVED_SOURCE_VERSION"] || - // AWS Amplify - https://docs.aws.amazon.com/amplify/latest/userguide/environment-variables.html - process.env["AWS_COMMIT_ID"] || - // Azure Pipelines - https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml - process.env["BUILD_SOURCEVERSION"] || - // Bitrise - https://devcenter.bitrise.io/builds/available-environment-variables/ - process.env["GIT_CLONE_COMMIT_HASH"] || - // Buddy CI - https://buddy.works/docs/pipelines/environment-variables#default-environment-variables - process.env["BUDDY_EXECUTION_REVISION"] || - // Builtkite - https://buildkite.com/docs/pipelines/environment-variables - process.env["BUILDKITE_COMMIT"] || - // CircleCI - https://circleci.com/docs/variables/ - process.env["CIRCLE_SHA1"] || - // Cirrus CI - https://cirrus-ci.org/guide/writing-tasks/#environment-variables - process.env["CIRRUS_CHANGE_IN_REPO"] || - // Codefresh - https://codefresh.io/docs/docs/codefresh-yaml/variables/ - process.env["CF_REVISION"] || - // Codemagic - https://docs.codemagic.io/yaml-basic-configuration/environment-variables/ - process.env["CM_COMMIT"] || - // Cloudflare Pages - https://developers.cloudflare.com/pages/platform/build-configuration/#environment-variables - process.env["CF_PAGES_COMMIT_SHA"] || - // Drone - https://docs.drone.io/pipeline/environment/reference/ - process.env["DRONE_COMMIT_SHA"] || - // Flightcontrol - https://www.flightcontrol.dev/docs/guides/flightcontrol/environment-variables#built-in-environment-variables - process.env["FC_GIT_COMMIT_SHA"] || - // Heroku #1 https://devcenter.heroku.com/articles/heroku-ci - process.env["HEROKU_TEST_RUN_COMMIT_VERSION"] || - // Heroku #2 https://docs.sentry.io/product/integrations/deployment/heroku/#configure-releases - process.env["HEROKU_SLUG_COMMIT"] || - // Railway - https://docs.railway.app/reference/variables#git-variables - process.env["RAILWAY_GIT_COMMIT_SHA"] || - // Render - https://render.com/docs/environment-variables - process.env["RENDER_GIT_COMMIT"] || - // Semaphore CI - https://docs.semaphoreci.com/ci-cd-environment/environment-variables - process.env["SEMAPHORE_GIT_SHA"] || - // TravisCI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables - process.env["TRAVIS_PULL_REQUEST_SHA"] || - // Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables - process.env["VERCEL_GIT_COMMIT_SHA"] || - process.env["VERCEL_GITHUB_COMMIT_SHA"] || - process.env["VERCEL_GITLAB_COMMIT_SHA"] || - process.env["VERCEL_BITBUCKET_COMMIT_SHA"] || - // Zeit (now known as Vercel) - process.env["ZEIT_GITHUB_COMMIT_SHA"] || - process.env["ZEIT_GITLAB_COMMIT_SHA"] || - process.env["ZEIT_BITBUCKET_COMMIT_SHA"]; - - const possibleReleaseNameOfCiProvidersWithGenericEnvVar = - // CloudBees CodeShip - https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables - process.env["CI_COMMIT_ID"] || - // Coolify - https://coolify.io/docs/knowledge-base/environment-variables - process.env["SOURCE_COMMIT"] || - // Heroku #3 https://devcenter.heroku.com/changelog-items/630 - process.env["SOURCE_VERSION"] || - // Jenkins - https://plugins.jenkins.io/git/#environment-variables - process.env["GIT_COMMIT"] || - // Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata - process.env["COMMIT_REF"] || - // TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html - process.env["BUILD_VCS_NUMBER"] || - // Woodpecker CI - https://woodpecker-ci.org/docs/usage/environment - process.env["CI_COMMIT_SHA"]; - - return ( - possibleReleaseNameOfGitProvider || - possibleReleaseNameOfCiProvidersWithSpecificEnvVar || - possibleReleaseNameOfCiProvidersWithGenericEnvVar || - gitRevision() - ); -} - -/** - * Generates code for the global injector which is responsible for setting the global - * `SENTRY_RELEASE` & `SENTRY_BUILD_INFO` variables. - */ -export function generateReleaseInjectorCode({ - release, - injectBuildInformation, -}: { - release: string; - injectBuildInformation: boolean; -}): CodeInjection { - let code = `e.SENTRY_RELEASE={id:${JSON.stringify(release)}};`; - - if (injectBuildInformation) { - const buildInfo = getBuildInformation(); - - code += `e.SENTRY_BUILD_INFO=${JSON.stringify(buildInfo)};`; - } - - return new CodeInjection(code); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function generateModuleMetadataInjectorCode(metadata: any): CodeInjection { - // We are merging the metadata objects in case modules are bundled twice with the plugin - // Use try-catch to avoid issues when bundlers rename global variables like 'window' to 'k' - return new CodeInjection( - `e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n; - nodeVersion: number | undefined; -} { - const packageJson = getPackageJson(); - - const { deps, depsVersions } = packageJson - ? getDependencies(packageJson) - : { deps: [], depsVersions: {} }; - - return { - deps, - depsVersions, - nodeVersion: parseMajorVersion(process.version), - }; -} - -export function stripQueryAndHashFromPath(path: string): string { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return path.split("?")[0]!.split("#")[0]!; -} - -export function replaceBooleanFlagsInCode( - code: string, - replacementValues: Record -): { code: string; map: SourceMap } | null { - const ms = new MagicString(code); - - Object.keys(replacementValues).forEach((key) => { - const value = replacementValues[key]; - - if (typeof value === "boolean") { - ms.replaceAll(key, JSON.stringify(value)); - } - }); - - if (ms.hasChanged()) { - return { - code: ms.toString(), - map: ms.generateMap({ hires: "boundary" }), - }; - } - - return null; -} - -// https://turbo.build/repo/docs/reference/system-environment-variables#environment-variables-in-tasks -export function getTurborepoEnvPassthroughWarning(envVarName: string): string { - return process.env["TURBO_HASH"] - ? `\nYou seem to be using Turborepo, did you forget to put ${envVarName} in \`passThroughEnv\`? https://turbo.build/repo/docs/reference/configuration#passthroughenv` - : ""; -} - -/** - * Gets the projects from the project option. This might be a single project or an array of projects. - */ -export function getProjects(project: string | string[] | undefined): string[] | undefined { - if (Array.isArray(project)) { - return project; - } - - if (project) { - return [project]; - } - - return undefined; -} - -/** - * Inlined functionality from @sentry/cli helper code to add `--ignore` options. - * - * Temporary workaround until we expose a function for injecting debug IDs. Currently, we directly call `execute` with CLI args to inject them. - */ -export function serializeIgnoreOptions(ignoreValue: string | string[] | undefined): string[] { - const DEFAULT_IGNORE = ["node_modules"]; - - const ignoreOptions: string[] = Array.isArray(ignoreValue) - ? ignoreValue - : typeof ignoreValue === "string" - ? [ignoreValue] - : DEFAULT_IGNORE; - - return ignoreOptions.reduce( - (acc, value) => acc.concat(["--ignore", String(value)]), - [] as string[] - ); -} - -/** - * Checks if a chunk contains only import/export statements and no substantial code. - * - * In Vite MPA (multi-page application) mode, HTML entry points create "facade" chunks - * that only contain import statements to load shared modules. These should not have - * Sentry code injected. However, in SPA mode, the main bundle also has an HTML facade - * but contains substantial application code that SHOULD have debug IDs injected. - * - * @ref https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829 - * @ref https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/839 - */ -export function containsOnlyImports(code: string): boolean { - const codeWithoutImports = code - // Remove side effect imports: import '/path'; or import "./path"; - // Using explicit negated character classes to avoid polynomial backtracking - .replace(/^\s*import\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "") - // Remove named/default imports: import x from '/path'; import { x } from '/path'; - .replace(/^\s*import\b[^'"`\n]*\bfrom\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "") - // Remove re-exports: export * from '/path'; export { x } from '/path'; - .replace(/^\s*export\b[^'"`\n]*\bfrom\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "") - // Remove block comments - .replace(/\/\*[\s\S]*?\*\//g, "") - // Remove line comments - .replace(/\/\/.*$/gm, "") - // Remove "use strict" directives - .replace(/["']use strict["']\s*;?/g, "") - .trim(); - - return codeWithoutImports.length === 0; -} - -export class CodeInjection { - // The code below is mostly ternary operators because it saves bundle size. - // The checks are to support as many environments as possible. (Node.js, Browser, webworkers, etc.) - private readonly header: string; - private readonly footer: string; - - constructor(private body: string = "") { - this.header = `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};`; - this.footer = "}catch(e){}}();"; - } - - public code(): string { - if (this.isEmpty()) { - return ""; - } - - return this.header + this.body + this.footer; - } - - public isEmpty(): boolean { - return this.body.length === 0; - } - - public append(code: CodeInjection | string): void { - if (code instanceof CodeInjection) { - this.body += code.body; - } else { - this.body += code; - } - } - - public clear(): void { - this.body = ""; - } - - public clone(): CodeInjection { - return new CodeInjection(this.body); - } -} diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts deleted file mode 100644 index 57d5b2b6..00000000 --- a/packages/bundler-plugins/src/esbuild/index.ts +++ /dev/null @@ -1,312 +0,0 @@ -import type { Options } from "../core"; -import { - createSentryBuildPluginManager, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, - getDebugIdSnippet, - createDebugIdUploadFunction, - CodeInjection, -} from "../core"; -import * as path from "node:path"; -import { createRequire } from "node:module"; -import { randomUUID } from "node:crypto"; - -interface EsbuildOnResolveArgs { - path: string; - kind: string; - importer?: string; - resolveDir: string; - pluginData?: unknown; -} - -interface EsbuildOnResolveResult { - path: string; - sideEffects?: boolean; - pluginName?: string; - namespace?: string; - suffix?: string; - pluginData?: unknown; -} - -interface EsbuildOnLoadArgs { - path: string; - pluginData?: unknown; -} - -interface EsbuildOnLoadResult { - loader: string; - pluginName: string; - contents: string; - resolveDir?: string; -} - -interface EsbuildOnEndArgs { - metafile?: { - outputs: Record; - }; -} - -interface EsbuildInitialOptions { - bundle?: boolean; - inject?: string[]; - metafile?: boolean; - define?: Record; -} - -interface EsbuildPluginBuild { - initialOptions: EsbuildInitialOptions; - onLoad: ( - options: { filter: RegExp; namespace?: string }, - callback: (args: EsbuildOnLoadArgs) => EsbuildOnLoadResult | null - ) => void; - onResolve: ( - options: { filter: RegExp }, - callback: (args: EsbuildOnResolveArgs) => EsbuildOnResolveResult | undefined - ) => void; - onEnd: (callback: (result: EsbuildOnEndArgs) => void | Promise) => void; -} - -function getEsbuildMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - esbuild transpiles this for us - const req = createRequire(import.meta.url); - const esbuild = req("esbuild") as { version?: string }; - // esbuild hasn't released a v1 yet, so we'll return the minor version as the major version - return esbuild.version?.split(".")[1]; - } catch { - // do nothing, we'll just not report a version - } - - return undefined; -} - -const pluginName = "sentry-esbuild-plugin"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function sentryEsbuildPlugin(userOptions: Options = {}): any { - const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { - loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[${pluginName}]`, - buildTool: "esbuild", - buildToolMajorVersion: getEsbuildMajorVersion(), - }); - - const { - logger, - normalizedOptions: options, - bundleSizeOptimizationReplacementValues: replacementValues, - bundleMetadata, - createDependencyOnBuildArtifacts, - } = sentryBuildPluginManager; - - if (options.disable) { - return { - name: "sentry-esbuild-noop-plugin", - setup() { - // noop plugin - }, - }; - } - - if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { - logger.warn( - "Running Sentry plugin from within a `node_modules` folder. Some features may not work." - ); - } - - const sourcemapsEnabled = options.sourcemaps?.disable !== true; - const staticInjectionCode = new CodeInjection(); - - if (!options.release.inject) { - logger.debug( - "Release injection disabled via `release.inject` option. Will not inject release." - ); - } else if (!options.release.name) { - logger.debug( - "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." - ); - } else { - staticInjectionCode.append( - generateReleaseInjectorCode({ - release: options.release.name, - injectBuildInformation: options._experiments.injectBuildInformation || false, - }) - ); - } - - if (Object.keys(bundleMetadata).length > 0) { - staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); - } - - // Component annotation warning - if (options.reactComponentAnnotation?.enabled) { - logger.warn( - "Component name annotation is not supported in esbuild. Please use a separate transform step or consider using a different bundler." - ); - } - - const transformReplace = Object.keys(replacementValues).length > 0; - - // Track entry points wrapped for debug ID injection - const debugIdWrappedPaths = new Set(); - - void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { - // Telemetry failures are acceptable - }); - - return { - name: pluginName, - setup({ initialOptions, onLoad, onResolve, onEnd }: EsbuildPluginBuild) { - // Release and/or metadata injection - if (!staticInjectionCode.isEmpty()) { - const virtualInjectionFilePath = path.resolve("_sentry-injection-stub"); - initialOptions.inject = initialOptions.inject || []; - initialOptions.inject.push(virtualInjectionFilePath); - - onResolve({ filter: /_sentry-injection-stub/ }, (args) => { - return { - path: args.path, - sideEffects: true, - pluginName, - }; - }); - - onLoad({ filter: /_sentry-injection-stub/ }, () => { - return { - loader: "js", - pluginName, - contents: staticInjectionCode.code(), - }; - }); - } - - // Bundle size optimizations - if (transformReplace) { - const replacementStringValues: Record = {}; - Object.entries(replacementValues).forEach(([key, value]) => { - replacementStringValues[key] = JSON.stringify(value); - }); - - initialOptions.define = { ...initialOptions.define, ...replacementStringValues }; - } - - // Debug ID injection - requires per-entry-point unique IDs - if (sourcemapsEnabled) { - // Clear state from previous builds (important for watch mode and test suites) - debugIdWrappedPaths.clear(); - - if (!initialOptions.bundle) { - logger.warn( - "The Sentry esbuild plugin only supports esbuild with `bundle: true` being set in the esbuild build options. Esbuild will probably crash now. Sorry about that. If you need to upload sourcemaps without `bundle: true`, it is recommended to use Sentry CLI instead: https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/cli/" - ); - } - - // Wrap entry points to inject debug IDs - onResolve({ filter: /.*/ }, (args) => { - if (args.kind !== "entry-point") { - return; - } - - // Skip injecting debug IDs into modules specified in the esbuild `inject` option - // since they're already part of the entry points - if (initialOptions.inject?.includes(args.path)) { - return; - } - - const resolvedPath = path.isAbsolute(args.path) - ? args.path - : path.join(args.resolveDir, args.path); - - // Skip injecting debug IDs into paths that have already been wrapped - if (debugIdWrappedPaths.has(resolvedPath)) { - return; - } - debugIdWrappedPaths.add(resolvedPath); - - return { - pluginName, - path: resolvedPath, - pluginData: { - isDebugIdProxy: true, - originalPath: args.path, - originalResolveDir: args.resolveDir, - }, - // We need to add a suffix here, otherwise esbuild will mark the entrypoint as resolved and won't traverse - // the module tree any further down past the proxy module because we're essentially creating a dependency - // loop back to the proxy module. - // By setting a suffix we're telling esbuild that the entrypoint and proxy module are two different things, - // making it re-resolve the entrypoint when it is imported from the proxy module. - // Super confusing? Yes. Works? Apparently... Let's see. - suffix: "?sentryDebugIdProxy=true", - }; - }); - - onLoad({ filter: /.*/ }, (args) => { - if (!(args.pluginData as { isDebugIdProxy?: boolean })?.isDebugIdProxy) { - return null; - } - - const originalPath = (args.pluginData as { originalPath: string }).originalPath; - const originalResolveDir = (args.pluginData as { originalResolveDir: string }) - .originalResolveDir; - - return { - loader: "js", - pluginName, - contents: ` - import "_sentry-debug-id-injection-stub"; - import * as OriginalModule from ${JSON.stringify(originalPath)}; - export default OriginalModule.default; - export * from ${JSON.stringify(originalPath)};`, - resolveDir: originalResolveDir, - }; - }); - - onResolve({ filter: /_sentry-debug-id-injection-stub/ }, (args) => { - return { - path: args.path, - sideEffects: true, - pluginName, - namespace: "sentry-debug-id-stub", - suffix: `?sentry-module-id=${randomUUID()}`, - }; - }); - - onLoad( - { filter: /_sentry-debug-id-injection-stub/, namespace: "sentry-debug-id-stub" }, - () => { - return { - loader: "js", - pluginName, - contents: getDebugIdSnippet(randomUUID()).code(), - }; - } - ); - } - - // Create release and optionally upload - const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); - const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); - - initialOptions.metafile = true; - onEnd(async (result) => { - try { - await sentryBuildPluginManager.createRelease(); - - if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { - const buildArtifacts = result.metafile ? Object.keys(result.metafile.outputs) : []; - await upload(buildArtifacts); - } - } finally { - freeGlobalDependencyOnBuildArtifacts(); - await sentryBuildPluginManager.deleteArtifacts(); - } - }); - }, - }; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default sentryEsbuildPlugin; -export type { Options as SentryEsbuildPluginOptions } from "../core"; -export { sentryCliBinaryExists } from "../core"; diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts deleted file mode 100644 index 926e4c47..00000000 --- a/packages/bundler-plugins/src/rollup/index.ts +++ /dev/null @@ -1,265 +0,0 @@ -import type { Options } from "../core"; -import { - createSentryBuildPluginManager, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, - isJsFile, - shouldSkipCodeInjection, - getDebugIdSnippet, - stringToUUID, - COMMENT_USE_STRICT_REGEX, - createDebugIdUploadFunction, - globFiles, - createComponentNameAnnotateHooks, - replaceBooleanFlagsInCode, - CodeInjection, -} from "../core"; -import type { SourceMap } from "magic-string"; -import MagicString from "magic-string"; -import type { TransformResult } from "rollup"; -import * as path from "node:path"; -import { createRequire } from "node:module"; - -function hasExistingDebugID(code: string): boolean { - // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) - const chunkStartSnippet = code.slice(0, 6000); - const chunkEndSnippet = code.slice(-500); - - if ( - chunkStartSnippet.includes("_sentryDebugIdIdentifier") || - chunkEndSnippet.includes("//# debugId=") - ) { - return true; // Debug ID already present, skip injection - } - - return false; -} - -function getRollupMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Rollup already transpiles this for us - const req = createRequire(import.meta.url); - const rollup = req("rollup") as { VERSION?: string }; - return rollup.VERSION?.split(".")[0]; - } catch { - // do nothing, we'll just not report a version - } - - return undefined; -} - -/** - * @ignore - this is the internal plugin factory function only used for the Vite plugin! - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function _rollupPluginInternal( - userOptions: Options = {}, - buildTool: "rollup" | "vite", - buildToolMajorVersion?: string -) { - const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { - loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[sentry-${buildTool}-plugin]`, - buildTool, - buildToolMajorVersion: buildToolMajorVersion || getRollupMajorVersion(), - }); - - const { - logger, - normalizedOptions: options, - bundleSizeOptimizationReplacementValues: replacementValues, - bundleMetadata, - createDependencyOnBuildArtifacts, - } = sentryBuildPluginManager; - - if (options.disable) { - return { - name: "sentry-noop-plugin", - }; - } - - if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { - logger.warn( - "Running Sentry plugin from within a `node_modules` folder. Some features may not work." - ); - } - - const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); - const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); - const sourcemapsEnabled = options.sourcemaps?.disable !== true; - const staticInjectionCode = new CodeInjection(); - - if (!options.release.inject) { - logger.debug( - "Release injection disabled via `release.inject` option. Will not inject release." - ); - } else if (!options.release.name) { - logger.debug( - "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." - ); - } else { - staticInjectionCode.append( - generateReleaseInjectorCode({ - release: options.release.name, - injectBuildInformation: options._experiments.injectBuildInformation || false, - }) - ); - } - - if (Object.keys(bundleMetadata).length > 0) { - staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); - } - - const transformAnnotations = options.reactComponentAnnotation?.enabled - ? createComponentNameAnnotateHooks( - options.reactComponentAnnotation?.ignoredComponents || [], - !!options.reactComponentAnnotation?._experimentalInjectIntoHtml - ) - : undefined; - - const transformReplace = Object.keys(replacementValues).length > 0; - const shouldTransform = transformAnnotations || transformReplace; - - function buildStart(): void { - void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { - // Telemetry failures are acceptable - }); - } - - async function transform(code: string, id: string): Promise { - // Component annotations are only in user code and boolean flag replacements are - // only in Sentry code. If we successfully add annotations, we can return early. - - if (transformAnnotations?.transform) { - const result = await transformAnnotations.transform(code, id); - if (result) { - return result; - } - } - - if (transformReplace) { - return replaceBooleanFlagsInCode(code, replacementValues); - } - - return null; - } - - function renderChunk( - code: string, - chunk: { fileName: string; facadeModuleId?: string | null }, - _?: unknown, - meta?: { magicString?: MagicString } - ): { - code: string; - map?: SourceMap; - } | null { - if (!isJsFile(chunk.fileName)) { - return null; // returning null means not modifying the chunk at all - } - - // Skip empty chunks and HTML facade chunks (Vite MPA) - if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { - return null; - } - - const injectCode = staticInjectionCode.clone(); - - if (sourcemapsEnabled && !hasExistingDebugID(code)) { - const debugId = stringToUUID(code); // generate a deterministic debug ID - injectCode.append(getDebugIdSnippet(debugId)); - } - - if (injectCode.isEmpty()) { - return null; - } - - const ms = meta?.magicString || new MagicString(code, { filename: chunk.fileName }); - const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; - - if (match) { - // Add injected code after any comments or "use strict" at the beginning of the bundle. - ms.appendLeft(match.length, injectCode.code()); - } else { - // ms.replace() doesn't work when there is an empty string match (which happens if - // there is neither, a comment, nor a "use strict" at the top of the chunk) so we - // need this special case here. - ms.prepend(injectCode.code()); - } - - // Rolldown can pass a native MagicString instance in meta.magicString - // https://rolldown.rs/in-depth/native-magic-string#usage-examples - if (ms?.constructor?.name === "BindingMagicString") { - // Rolldown docs say to return the magic string instance directly in this case - return { code: ms as unknown as string }; - } - - return { - code: ms.toString(), - map: ms.generateMap({ file: chunk.fileName, hires: "boundary" as unknown as undefined }), - }; - } - - async function writeBundle( - outputOptions: { dir?: string; file?: string }, - bundle: { [fileName: string]: unknown } - ): Promise { - try { - await sentryBuildPluginManager.createRelease(); - - if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { - if (outputOptions.dir) { - const outputDir = outputOptions.dir; - const JS_AND_MAP_PATTERNS = [ - "/**/*.js", - "/**/*.mjs", - "/**/*.cjs", - "/**/*.js.map", - "/**/*.mjs.map", - "/**/*.cjs.map", - ].map((q) => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files - const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir }); - await upload(buildArtifacts); - } else if (outputOptions.file) { - await upload([outputOptions.file]); - } else { - const buildArtifacts = Object.keys(bundle).map((asset) => - path.join(path.resolve(), asset) - ); - await upload(buildArtifacts); - } - } - } finally { - freeGlobalDependencyOnBuildArtifacts(); - await sentryBuildPluginManager.deleteArtifacts(); - } - } - - const name = `sentry-${buildTool}-plugin`; - - if (shouldTransform) { - return { - name, - buildStart, - transform, - renderChunk, - writeBundle, - }; - } - - return { - name, - buildStart, - renderChunk, - writeBundle, - }; -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any -export function sentryRollupPlugin(userOptions: Options = {}): any { - // We return an array here so we don't break backwards compatibility with what - // unplugin used to return - return [_rollupPluginInternal(userOptions, "rollup")]; -} - -export type { Options as SentryRollupPluginOptions } from "../core"; -export { sentryCliBinaryExists } from "../core"; diff --git a/packages/bundler-plugins/src/vite/index.ts b/packages/bundler-plugins/src/vite/index.ts deleted file mode 100644 index 6ca804ea..00000000 --- a/packages/bundler-plugins/src/vite/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { SentryRollupPluginOptions } from "../rollup"; -import { _rollupPluginInternal } from "../rollup"; -import { createRequire } from "node:module"; - -interface SentryVitePlugin { - name: string; - enforce: "pre"; -} - -function getViteMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Rollup already transpiles this for us - const req = createRequire(import.meta.url); - const vite = req("vite") as { version?: string }; - return vite.version?.split(".")[0]; - } catch { - // do nothing, we'll just not report a version - } - - return undefined; -} - -export const sentryVitePlugin = (options?: SentryRollupPluginOptions): SentryVitePlugin[] => { - return [ - { - enforce: "pre", - ..._rollupPluginInternal(options, "vite", getViteMajorVersion()), - }, - ]; -}; - -export type { Options as SentryVitePluginOptions } from "../core"; -export { sentryCliBinaryExists } from "../core"; diff --git a/packages/bundler-plugins/src/webpack/component-annotation-transform.ts b/packages/bundler-plugins/src/webpack/component-annotation-transform.ts deleted file mode 100644 index bf6c6612..00000000 --- a/packages/bundler-plugins/src/webpack/component-annotation-transform.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Webpack loader for component annotation transform -// Based on unplugin v1.0.1 transform loader pattern - -export default async function transform( - this: { - async: () => (err: Error | null, content?: string, sourceMap?: unknown) => void; - resourcePath: string; - query: { - transform?: ( - code: string, - id: string - ) => Promise<{ code: string; map?: unknown } | null | undefined | string>; - }; - }, - source: string, - map: unknown -): Promise { - const callback = this.async(); - const { transform: transformFn } = this.query; - - if (!transformFn) { - return callback(null, source, map); - } - - try { - const id = this.resourcePath; - const result = await transformFn(source, id); - - if (result == null) { - callback(null, source, map); - } else if (typeof result === "string") { - callback(null, result, map); - } else { - callback(null, result.code, result.map || map); - } - } catch (error) { - if (error instanceof Error) { - callback(error); - } else { - callback(new Error(String(error))); - } - } -} diff --git a/packages/bundler-plugins/src/webpack/index.ts b/packages/bundler-plugins/src/webpack/index.ts deleted file mode 100644 index 8f3fc523..00000000 --- a/packages/bundler-plugins/src/webpack/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { SentryWebpackPluginOptions } from "./webpack4and5"; -import { sentryWebpackPluginFactory } from "./webpack4and5"; -import { createRequire } from "node:module"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PluginClass = new (options: any) => unknown; - -type WebpackModule = { - BannerPlugin?: PluginClass; - DefinePlugin?: PluginClass; - default?: WebpackModule; -}; - -// `webpack` is an optional peer dependency. We require it lazily so the plugin doesn't -// crash on load in bundlers that don't ship `webpack` (e.g. rspack) — those provide -// the plugin classes via `compiler.webpack` at runtime instead. -function loadWebpack(): WebpackModule { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Rollup transpiles import.meta for CJS - return createRequire(import.meta.url)("webpack") as WebpackModule; - } catch { - return {}; - } -} - -const webpack = loadWebpack(); -const BannerPlugin = webpack.BannerPlugin ?? webpack.default?.BannerPlugin; -const DefinePlugin = webpack.DefinePlugin ?? webpack.default?.DefinePlugin; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = - sentryWebpackPluginFactory({ - BannerPlugin, - DefinePlugin, - }); - -export { sentryCliBinaryExists } from "../core"; - -export type { SentryWebpackPluginOptions }; diff --git a/packages/bundler-plugins/src/webpack/webpack4and5.ts b/packages/bundler-plugins/src/webpack/webpack4and5.ts deleted file mode 100644 index 4a006dc8..00000000 --- a/packages/bundler-plugins/src/webpack/webpack4and5.ts +++ /dev/null @@ -1,347 +0,0 @@ -import type { Options } from "../core/index"; -import { - createSentryBuildPluginManager, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, - stringToUUID, - createComponentNameAnnotateHooks, - CodeInjection, - getDebugIdSnippet, - createDebugIdUploadFunction, -} from "../core/index"; -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { createRequire } from "node:module"; -import { randomUUID } from "node:crypto"; - -const _req = createRequire(import.meta.url); - -// Resolve the loader path via the package's own exports. -// webpack4and5.ts may end up in a shared chunk (_chunks/) whose import.meta.url -// does not point to the webpack/ directory where the transform file lives, so -// a path-relative lookup would fail. Using require.resolve on the package export -// always finds the correct installed file regardless of chunk placement. -let COMPONENT_ANNOTATION_LOADER: string; -try { - COMPONENT_ANNOTATION_LOADER = _req.resolve("@sentry/bundler-plugins/webpack-loader"); -} catch { - // Fallback for non-packaged environments (e.g., monorepo source runs without dist) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Rollup transpiles import.meta for us for CJS - const dirname = path.dirname(fileURLToPath(import.meta.url)); - COMPONENT_ANNOTATION_LOADER = path.resolve( - dirname, - typeof __dirname !== "undefined" - ? "component-annotation-transform.js" // CJS - : "component-annotation-transform.mjs" // ESM - ); -} - -// since webpack 5.1 compiler contains webpack module so plugins always use correct webpack version -// https://github.com/webpack/webpack/commit/65eca2e529ce1d79b79200d4bdb1ce1b81141459 - -interface BannerPluginCallbackArg { - chunk?: { - hash?: string; - contentHash?: { - javascript?: string; - }; - }; -} - -type UnsafeBannerPlugin = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (options: any): unknown; -}; - -type UnsafeDefinePlugin = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (options: any): unknown; -}; - -type WebpackModule = { - resource?: string; -}; - -type WebpackLoaderCallback = (err: Error | null, content?: string, sourceMap?: unknown) => void; - -type WebpackLoaderContext = { - callback: WebpackLoaderCallback; -}; - -type WebpackCompilationContext = { - compiler: { - webpack?: { - NormalModule?: { - getCompilationHooks: (compilation: WebpackCompilationContext) => { - loader: { - tap: ( - name: string, - callback: (loaderContext: WebpackLoaderContext, module: WebpackModule) => void - ) => void; - }; - }; - }; - }; - }; - hooks: { - normalModuleLoader?: { - tap: ( - name: string, - callback: (loaderContext: WebpackLoaderContext, module: WebpackModule) => void - ) => void; - }; - }; -}; - -type WebpackCompiler = { - options: { - plugins?: unknown[]; - mode?: string; - module?: { - rules?: unknown[]; - }; - }; - hooks: { - thisCompilation: { - tap: (name: string, callback: (compilation: WebpackCompilationContext) => void) => void; - }; - afterEmit: { - tapAsync: ( - name: string, - callback: (compilation: WebpackCompilation, cb: () => void) => void - ) => void; - }; - done: { - tap: (name: string, callback: () => void) => void; - }; - }; - webpack?: { - BannerPlugin?: UnsafeBannerPlugin; - DefinePlugin?: UnsafeDefinePlugin; - }; -}; - -type WebpackCompilation = { - outputOptions: { - path?: string; - }; - assets: Record; - hooks: { - processAssets: { - tap: (options: { name: string; stage: number }, callback: () => void) => void; - }; - }; -}; - -// Detect webpack major version for telemetry (helps differentiate webpack 4 vs 5 usage) -function getWebpackMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Rollup already transpiles this for us - const req = createRequire(import.meta.url); - const webpack = req("webpack") as { version?: string; default?: { version?: string } }; - const version = webpack?.version ?? webpack?.default?.version; - const webpackMajorVersion = version?.split(".")[0]; // "4" or "5" - return webpackMajorVersion; - } catch { - return undefined; - } -} - -/** - * The factory function accepts BannerPlugin and DefinePlugin classes in - * order to avoid direct dependencies on webpack. - * - * This allow us to export version of the plugin for webpack 5.1+ and compatible environments. - * - * Since webpack 5.1 compiler contains webpack module so plugins always use correct webpack version. - */ -export function sentryWebpackPluginFactory({ - BannerPlugin: UnsafeBannerPlugin, - DefinePlugin: UnsafeDefinePlugin, -}: { - BannerPlugin?: UnsafeBannerPlugin; - DefinePlugin?: UnsafeDefinePlugin; -} = {}) { - return function sentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) { - const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { - loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? "[sentry-webpack-plugin]", - buildTool: "webpack", - buildToolMajorVersion: getWebpackMajorVersion(), - }); - - const { - logger, - normalizedOptions: options, - bundleSizeOptimizationReplacementValues: replacementValues, - bundleMetadata, - createDependencyOnBuildArtifacts, - } = sentryBuildPluginManager; - - if (options.disable) { - return { - apply() { - // noop plugin - }, - }; - } - - if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { - logger.warn( - "Running Sentry plugin from within a `node_modules` folder. Some features may not work." - ); - } - - const sourcemapsEnabled = options.sourcemaps?.disable !== true; - const staticInjectionCode = new CodeInjection(); - - if (!options.release.inject) { - logger.debug( - "Release injection disabled via `release.inject` option. Will not inject release." - ); - } else if (!options.release.name) { - logger.debug( - "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." - ); - } else { - staticInjectionCode.append( - generateReleaseInjectorCode({ - release: options.release.name, - injectBuildInformation: options._experiments.injectBuildInformation || false, - }) - ); - } - - if (Object.keys(bundleMetadata).length > 0) { - staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); - } - - const transformAnnotations = options.reactComponentAnnotation?.enabled - ? createComponentNameAnnotateHooks( - options.reactComponentAnnotation?.ignoredComponents || [], - !!options.reactComponentAnnotation?._experimentalInjectIntoHtml - ) - : undefined; - - const transformReplace = Object.keys(replacementValues).length > 0; - - return { - apply(compiler: WebpackCompiler) { - void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { - // Telemetry failures are acceptable - }); - - // Get the correct plugin classes (webpack 5.1+ vs older versions) - const BannerPlugin = compiler?.webpack?.BannerPlugin || UnsafeBannerPlugin; - const DefinePlugin = compiler?.webpack?.DefinePlugin || UnsafeDefinePlugin; - - // Add BannerPlugin for code injection (release, metadata, debug IDs) - if (!staticInjectionCode.isEmpty() || sourcemapsEnabled) { - if (!BannerPlugin) { - logger.warn( - "BannerPlugin is not available. Skipping code injection. This usually means webpack is not properly configured." - ); - } else { - compiler.options.plugins = compiler.options.plugins || []; - compiler.options.plugins.push( - new BannerPlugin({ - raw: true, - include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/, - banner: (arg?: BannerPluginCallbackArg) => { - const codeToInject = staticInjectionCode.clone(); - if (sourcemapsEnabled) { - const hash = arg?.chunk?.contentHash?.javascript ?? arg?.chunk?.hash; - const debugId = hash ? stringToUUID(hash) : randomUUID(); - codeToInject.append(getDebugIdSnippet(debugId)); - } - return codeToInject.code(); - }, - }) - ); - } - } - - // Add DefinePlugin for bundle size optimizations - if (transformReplace && DefinePlugin) { - compiler.options.plugins = compiler.options.plugins || []; - compiler.options.plugins.push(new DefinePlugin(replacementValues)); - } - - // Add component name annotation transform - if (transformAnnotations?.transform) { - compiler.options.module = compiler.options.module || {}; - compiler.options.module.rules = compiler.options.module.rules || []; - compiler.options.module.rules.unshift({ - test: /\.[jt]sx$/, - exclude: /node_modules/, - enforce: "pre", - use: [ - { - loader: COMPONENT_ANNOTATION_LOADER, - options: { - transform: transformAnnotations.transform, - }, - }, - ], - }); - } - - compiler.hooks.afterEmit.tapAsync( - "sentry-webpack-plugin", - (compilation: WebpackCompilation, callback: (err?: Error) => void) => { - const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); - const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); - - const run = async (): Promise => { - try { - await sentryBuildPluginManager.createRelease(); - if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { - const outputPath = compilation.outputOptions.path ?? path.resolve(); - const buildArtifacts = Object.keys(compilation.assets).map((asset) => - path.join(outputPath, asset) - ); - await upload(buildArtifacts); - } - } finally { - freeGlobalDependencyOnBuildArtifacts(); - await sentryBuildPluginManager.deleteArtifacts(); - } - }; - - run().then( - () => callback(), - (err: Error) => callback(err) - ); - } - ); - - if ( - userOptions._experiments?.forceExitOnBuildCompletion && - compiler.options.mode === "production" - ) { - compiler.hooks.done.tap("sentry-webpack-plugin", () => { - setTimeout(() => { - logger.debug("Exiting process after debug file upload"); - process.exit(0); - }); - }); - } - }, - }; - }; -} - -export type SentryWebpackPluginOptions = Options & { - _experiments?: Options["_experiments"] & { - /** - * If enabled, the webpack plugin will exit the build process after the build completes. - * Use this with caution, as it will terminate the process. - * - * More information: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/345 - * - * @default false - */ - forceExitOnBuildCompletion?: boolean; - }; -}; diff --git a/packages/bundler-plugins/src/webpack/webpack5.ts b/packages/bundler-plugins/src/webpack/webpack5.ts deleted file mode 100644 index 410b660c..00000000 --- a/packages/bundler-plugins/src/webpack/webpack5.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { SentryWebpackPluginOptions } from "./webpack4and5"; -import { sentryWebpackPluginFactory } from "./webpack4and5"; - -const createSentryWebpackPlugin = sentryWebpackPluginFactory(); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = - createSentryWebpackPlugin; - -export { sentryCliBinaryExists } from "../core"; - -export type { SentryWebpackPluginOptions }; diff --git a/packages/bundler-plugins/test/babel-plugin/__snapshots__/test-plugin.test.ts.snap b/packages/bundler-plugins/test/babel-plugin/__snapshots__/test-plugin.test.ts.snap deleted file mode 100644 index 5d626df1..00000000 --- a/packages/bundler-plugins/test/babel-plugin/__snapshots__/test-plugin.test.ts.snap +++ /dev/null @@ -1,740 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Fragment Detection > combines all fragment patterns correctly 1`] = ` -"import React, { Fragment as ImportedF } from 'react'; -import * as MyReact from 'react'; -const { - Fragment: DestructuredF -} = React; -const { - Fragment -} = MyReact; -const AssignedF = Fragment; // ← This uses the destructured Fragment from MyReact - -export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - className: "container", - "data-sentry-component": "TestComponent", - "data-sentry-source-file": "filename-test.js" - }, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", null, "JSX Fragment content")), /*#__PURE__*/React.createElement(ImportedF, null, /*#__PURE__*/React.createElement("span", null, "Imported alias content")), /*#__PURE__*/React.createElement(DestructuredF, null, /*#__PURE__*/React.createElement("span", null, "Destructured content")), /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("span", null, "Namespace destructured content")), /*#__PURE__*/React.createElement(AssignedF, null, /*#__PURE__*/React.createElement("span", null, "Variable assigned content")), /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", null, "React.Fragment content")), /*#__PURE__*/React.createElement(MyReact.Fragment, null, /*#__PURE__*/React.createElement("span", null, "Namespace Fragment content"))); -}" -`; - -exports[`Fragment Detection > handles Fragment aliased correctly when used by other non-Fragment components in a different scope 1`] = ` -"import { Fragment as OriginalF } from 'react'; -import { OtherComponent } from 'some-library'; -function TestComponent() { - const F = OriginalF; - - // Use Fragment alias - should be ignored - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", null, "This should NOT have data-sentry-element (Fragment)")); -} -function AnotherComponent() { - // Different component with same alias name in different function scope - const F = OtherComponent; - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", null, "This SHOULD have data-sentry-element (not Fragment)")); -}" -`; - -exports[`Fragment Detection > handles complex variable assignment chains 1`] = ` -"import { Fragment } from 'react'; -const MyFragment = Fragment; -const AnotherFragment = MyFragment; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(AnotherFragment, null, /*#__PURE__*/React.createElement("div", null, "Content in chained fragment")); -}" -`; - -exports[`Fragment Detection > handles destructuring from aliased React imports 1`] = ` -"import MyReact from 'react'; -const { - Fragment -} = MyReact; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content from aliased React destructuring")); -}" -`; - -exports[`Fragment Detection > handles destructuring from namespace imports 1`] = ` -"import * as ReactLib from 'react'; -const { - Fragment: F -} = ReactLib; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", null, "Content from namespace destructuring")); -}" -`; - -exports[`Fragment Detection > handles multiple destructuring patterns in one file 1`] = ` -"import React from 'react'; -import * as MyReact from 'react'; -const { - Fragment -} = React; -const { - Fragment: AliasedFrag -} = MyReact; -export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent", - "data-sentry-source-file": "filename-test.js" - }, /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("span", null, "Regular destructured")), /*#__PURE__*/React.createElement(AliasedFrag, null, /*#__PURE__*/React.createElement("p", null, "Aliased destructured"))); -}" -`; - -exports[`Fragment Detection > ignores Fragment assigned to variable 1`] = ` -"import { Fragment } from 'react'; -const MyFragment = Fragment; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyFragment, null, /*#__PURE__*/React.createElement("div", null, "Content in variable fragment")); -}" -`; - -exports[`Fragment Detection > ignores Fragment from React destructuring 1`] = ` -"import React from 'react'; -const { - Fragment -} = React; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content in destructured fragment")); -}" -`; - -exports[`Fragment Detection > ignores Fragment from mixed destructuring 1`] = ` -"import React from 'react'; -const { - Fragment, - createElement, - useState -} = React; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content with other destructured items")); -}" -`; - -exports[`Fragment Detection > ignores Fragment imported with alias 1`] = ` -"import { Fragment as F } from 'react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", null, "Content in aliased fragment")); -}" -`; - -exports[`Fragment Detection > ignores Fragment with React namespace alias 1`] = ` -"import * as MyReact from 'react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyReact.Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content in namespaced fragment")); -}" -`; - -exports[`Fragment Detection > ignores Fragment with destructuring alias 1`] = ` -"import React from 'react'; -const { - Fragment: MyFragment -} = React; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyFragment, null, /*#__PURE__*/React.createElement("div", null, "Content in aliased destructured fragment")); -}" -`; - -exports[`Fragment Detection > ignores JSX fragments (<>) 1`] = ` -"export default function TestComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content in JSX fragment"), /*#__PURE__*/React.createElement("span", null, "More content")); -}" -`; - -exports[`Fragment Detection > ignores React default import with Fragment 1`] = ` -"import MyReact from 'react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyReact.Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content in default import fragment")); -}" -`; - -exports[`Fragment Detection > ignores React.Fragment with member expression handling 1`] = ` -"import React from 'react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", null, "Content")); -}" -`; - -exports[`Fragment Detection > ignores multiple fragment patterns in same file 1`] = ` -"import React, { Fragment } from 'react'; -const MyFragment = Fragment; -export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent", - "data-sentry-source-file": "filename-test.js" - }, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", null, "JSX Fragment content")), /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("span", null, "Direct Fragment content")), /*#__PURE__*/React.createElement(MyFragment, null, /*#__PURE__*/React.createElement("p", null, "Variable Fragment content")), /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "React.Fragment content"))); -}" -`; - -exports[`Fragment Detection > works with annotate-fragments option disabled 1`] = ` -"import { Fragment as F } from 'react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", null, "Content")); -}" -`; - -exports[`Fragment Detection > works with annotate-fragments option enabled 1`] = ` -"import { Fragment as F } from 'react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent", - "data-sentry-source-file": "filename-test.js" - }, "Content")); -}" -`; - -exports[`arrow snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -}; -export default componentName;" -`; - -exports[`arrow-anonymous-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => { - return (() => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")))(); -}; -export default componentName;" -`; - -exports[`arrow-anonymous-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => { - return (() => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")))(); -}; -export default componentName;" -`; - -exports[`arrow-anonymous-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => { - return (() => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")))(); -}; -export default componentName;" -`; - -exports[`arrow-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -}; -export default componentName;" -`; - -exports[`arrow-noreturn snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" -}, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -export default componentName;" -`; - -exports[`arrow-noreturn-annotate-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" -}, "Hello world")); -export default componentName;" -`; - -exports[`arrow-noreturn-annotate-fragment-no-whitespace snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" -}, "Hello world"), /*#__PURE__*/React.createElement("h1", null, "Hola Sol")); -export default componentName;" -`; - -exports[`arrow-noreturn-annotate-fragment-once snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" -}, "Hello world"), /*#__PURE__*/React.createElement("h1", null, "Hola Sol")); -export default componentName;" -`; - -exports[`arrow-noreturn-annotate-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" -}, "Hello world")); -export default componentName;" -`; - -exports[`arrow-noreturn-annotate-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" -}, "Hello world")); -export default componentName;" -`; - -exports[`arrow-noreturn-annotate-trivial-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, "Hello world"); -export default componentName;" -`; - -exports[`arrow-noreturn-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -export default componentName;" -`; - -exports[`arrow-noreturn-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -export default componentName;" -`; - -exports[`arrow-noreturn-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -export default componentName;" -`; - -exports[`arrow-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -}; -export default componentName;" -`; - -exports[`arrow-shorthand-fragment snapshot matches 1`] = ` -"import React from 'react'; -const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -}; -export default componentName;" -`; - -exports[`component snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } -} -export default componentName;" -`; - -exports[`component-annotate-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "A"); - } -} -export default componentName;" -`; - -exports[`component-annotate-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" - }, "Hello world")); - } -} -export default componentName;" -`; - -exports[`component-annotate-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" - }, "Hello world")); - } -} -export default componentName;" -`; - -exports[`component-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(Fragment, null, "A"); - } -} -export default componentName;" -`; - -exports[`component-fragment-native snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(Fragment, null, "A"); - } -} -export default componentName;" -`; - -exports[`component-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "A"); - } -} -export default componentName;" -`; - -exports[`component-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "A"); - } -} -export default componentName;" -`; - -exports[`handles nested member expressions in component names 1`] = ` -"import React from 'react'; -import { Components } from 'my-ui-library'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent", - "data-sentry-source-file": "filename-test.js" - }, /*#__PURE__*/React.createElement(Components.UI.Button, null, "Click me"), /*#__PURE__*/React.createElement(Components.UI.Card.Header, { - "data-sentry-element": "Components.UI.Card.Header", - "data-sentry-source-file": "filename-test.js" - }, "Title")); -}" -`; - -exports[`handles ternary operation returned by function body 1`] = ` -"const maybeTrue = Math.random() > 0.5; -export default function componentName() { - return maybeTrue ? '' : /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent", - "data-sentry-component": "componentName" - }); -}" -`; - -exports[`ignores components with member expressions when in ignoredComponents 1`] = ` -"import React from 'react'; -import { Tab } from '@headlessui/react'; -export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent", - "data-sentry-source-file": "filename-test.js" - }, /*#__PURE__*/React.createElement(Tab.Group, null, /*#__PURE__*/React.createElement(Tab.List, null, /*#__PURE__*/React.createElement(Tab, { - "data-sentry-element": "Tab", - "data-sentry-source-file": "filename-test.js" - }, "Tab 1"), /*#__PURE__*/React.createElement(Tab, { - "data-sentry-element": "Tab", - "data-sentry-source-file": "filename-test.js" - }, "Tab 2")), /*#__PURE__*/React.createElement(Tab.Panels, null, /*#__PURE__*/React.createElement(Tab.Panel, null, "Content 1"), /*#__PURE__*/React.createElement(Tab.Panel, null, "Content 2")))); -}" -`; - -exports[`nonJSX snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class TestClass extends Component { - test() { - return true; - } -} -export default TestClass;" -`; - -exports[`option-attribute snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -}; -export default componentName;" -`; - -exports[`option-format snapshot matches 1`] = ` -"import React, { Component } from 'react'; -const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); -}; -export default componentName;" -`; - -exports[`pure snapshot matches 1`] = ` -"import React from 'react'; -class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "PureComponentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } -} -export default PureComponentName;" -`; - -exports[`pure-native snapshot matches 1`] = ` -"import React from 'react'; -class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement("div", { - dataSentryComponent: "PureComponentName", - dataSentrySourceFile: "filename-test.js" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } -} -export default PureComponentName;" -`; - -exports[`pureComponent-fragment snapshot matches 1`] = ` -"import React, { Fragment } from 'react'; -class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } -} -export default PureComponentName;" -`; - -exports[`pureComponent-react-fragment snapshot matches 1`] = ` -"import React from 'react'; -class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } -} -export default PureComponentName;" -`; - -exports[`pureComponent-shorthand-fragment snapshot matches 1`] = ` -"import React from 'react'; -class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } -} -export default PureComponentName;" -`; - -exports[`rawfunction snapshot matches 1`] = ` -"import React, { Component } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "SubComponent" - }, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent" - })); -}; -export default componentName;" -`; - -exports[`rawfunction-annotate-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent", - "data-sentry-component": "componentName" - })); -}; -export default componentName;" -`; - -exports[`rawfunction-annotate-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent", - "data-sentry-component": "componentName" - })); -}; -export default componentName;" -`; - -exports[`rawfunction-annotate-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent", - "data-sentry-component": "componentName" - })); -}; -export default componentName;" -`; - -exports[`rawfunction-fragment snapshot matches 1`] = ` -"import React, { Component, Fragment } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent" - })); -}; -export default componentName;" -`; - -exports[`rawfunction-react-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent" - })); -}; -export default componentName;" -`; - -exports[`rawfunction-shorthand-fragment snapshot matches 1`] = ` -"import React, { Component } from 'react'; -function SubComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "Sub"); -} -const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, { - "data-sentry-element": "SubComponent" - })); -}; -export default componentName;" -`; - -exports[`tags snapshot matches 1`] = ` -"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' - } -});" -`; - -exports[`unknown-element snapshot matches 1`] = ` -"import React, { Component } from 'react'; -class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement("bogus", { - "data-sentry-element": "bogus", - "data-sentry-component": "componentName", - "data-sentry-source-file": "filename-test.js" - }, /*#__PURE__*/React.createElement("h1", null, "A")); - } -} -export default componentName;" -`; diff --git a/packages/bundler-plugins/test/babel-plugin/experimental.test.ts b/packages/bundler-plugins/test/babel-plugin/experimental.test.ts deleted file mode 100644 index 29b04dee..00000000 --- a/packages/bundler-plugins/test/babel-plugin/experimental.test.ts +++ /dev/null @@ -1,2059 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2020 Engineering at FullStory - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { describe, it, expect } from "vitest"; -import { transform } from "@babel/core"; -import { experimentalComponentNameAnnotatePlugin as plugin } from "../../src/babel-plugin/index"; - -const BananasPizzaAppStandardInput = `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 ; - } -} - -class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { text: '' }; - } - - render() { - return - this.setState({ text })} value={this.state.text} /> - - {this.state.text.split(' ').map(word => word && '🍕').join(' ')} - - ; - } -} - -export default function App() { - return - FullStory ReactNative testing app - - - ; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } -});`; - -it("unknown-element snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return

A

; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement("bogus", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "A")); - } - } - export default componentName;" - `); -}); - -it("component-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -class componentName extends Component { - render() { - return A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(Fragment, null, "A"); - } - } - export default componentName;" - `); -}); - -it("component-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "A"); - } - } - export default componentName;" - `); -}); - -it("component-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return <>A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "A"); - } - } - export default componentName;" - `); -}); - -it("component-annotate-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return <> -

Hello world

- ; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - } - } - export default componentName;" - `); -}); - -it("arrow-noreturn-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( - -

Hello world

-
-); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - export default componentName;" - `); -}); - -it("arrow-noreturn-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( - <> -

Hello world

- -); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - export default componentName;" - `); -}); - -it("arrow-noreturn-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( - -

Hello world

-
-); - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - export default componentName;" - `); -}); - -it("arrow-noreturn-annotate-trivial-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( - Hello world -); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, "Hello world"); - export default componentName;" - `); -}); - -it("arrow snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return
-

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - }; - export default componentName;" - `); -}); - -it("option-attribute snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return
-

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - }; - export default componentName;" - `); -}); - -it("component snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return
-

Hello world

-
; - } -} - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } - } - export default componentName;" - `); -}); - -it("rawfunction-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -function SubComponent() { - return Sub; -} - -const componentName = () => { - return - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - function SubComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, "Sub"); - } - const componentName = () => { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(SubComponent, null)); - }; - export default componentName;" - `); -}); - -it("rawfunction-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return Sub; -} - -const componentName = () => { - return - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - function SubComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "Sub"); - } - const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, null)); - }; - export default componentName;" - `); -}); - -it("rawfunction-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return <>Sub; -} - -const componentName = () => { - return <> - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - function SubComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, "Sub"); - } - const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, null)); - }; - export default componentName;" - `); -}); - -it("arrow-noreturn snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( -
-

Hello world

-
-); - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - export default componentName;" - `); -}); - -it("tags snapshot matches", () => { - const result = transform( - `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 ; - } -} - -class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { text: '' }; - } - - render() { - return - this.setState({ text })} value={this.state.text} /> - - {this.state.text.split(' ').map(word => word && '🍕').join(' ')} - - ; - } -} - -export default function App() { - return - FullStory ReactNative testing app - - - ; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } -}); -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - 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"; - 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", - dataSentryComponent: "Bananas" - }); - } - } - class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { - text: '' - }; - } - render() { - return /*#__PURE__*/React.createElement(View, { - style: { - padding: 10 - }, - dataSentryComponent: "PizzaTranslator" - }, /*#__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, - dataSentryComponent: "App" - }, /*#__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' - } - });" - `); -}); - -it("option-format snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return
-

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - }; - export default componentName;" - `); -}); - -it("pureComponent-fragment snapshot matches", () => { - const result = transform( - `import React, { Fragment } from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return -

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Fragment } from 'react'; - class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "PureComponentName" - }, "Hello world")); - } - } - export default PureComponentName;" - `); -}); - -it("pureComponent-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return <> -

Hello world

- ; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "PureComponentName" - }, "Hello world")); - } - } - export default PureComponentName;" - `); -}); - -it("pureComponent-react-fragment snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return -

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "PureComponentName" - }, "Hello world")); - } - } - export default PureComponentName;" - `); -}); - -it("rawfunction snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return
Sub
; -} - -const componentName = () => { - return
- -
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - function SubComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "SubComponent" - }, "Sub"); - } - const componentName = () => { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "componentName" - }, /*#__PURE__*/React.createElement(SubComponent, null)); - }; - export default componentName;" - `); -}); - -it("arrow-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => { - return -

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - const componentName = () => { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - }; - export default componentName;" - `); -}); - -it("arrow-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React from 'react'; - -const componentName = () => { - return <> -

Hello world

- ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - }; - export default componentName;" - `); -}); - -it("arrow-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return -

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", { - "data-sentry-component": "componentName" - }, "Hello world")); - }; - export default componentName;" - `); -}); - -it("nonJSX snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class TestClass extends Component { - test() { - return true; - } -} - -export default TestClass; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - class TestClass extends Component { - test() { - return true; - } - } - export default TestClass;" - `); -}); - -it("arrow-anonymous-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => { - return (() => -

Hello world

-
)(); -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - const componentName = () => { - return (() => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")))(); - }; - export default componentName;" - `); -}); - -it("arrow-anonymous-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return (() => <> -

Hello world

- )(); -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => { - return (() => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")))(); - }; - export default componentName;" - `); -}); - -it("arrow-anonymous-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return (() => -

Hello world

-
)(); -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component } from 'react'; - const componentName = () => { - return (() => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello world")))(); - }; - export default componentName;" - `); -}); - -it("pure snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return
-

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "PureComponentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } - } - export default PureComponentName;" - `); -}); - -it("component-fragment-native snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -class componentName extends Component { - render() { - return A; - } -} - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true }]], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Component, Fragment } from 'react'; - class componentName extends Component { - render() { - return /*#__PURE__*/React.createElement(Fragment, null, "A"); - } - } - export default componentName;" - `); -}); - -it("pure-native snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return
-

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true }]], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - class PureComponentName extends React.PureComponent { - render() { - return /*#__PURE__*/React.createElement("div", { - dataSentryComponent: "PureComponentName" - }, /*#__PURE__*/React.createElement("h1", null, "Hello world")); - } - } - export default PureComponentName;" - `); -}); - -it("skips components marked in ignoredComponents", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - presets: ["@babel/preset-react"], - 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'; - 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 - }, - dataSentryComponent: "PizzaTranslator" - }, /*#__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, - dataSentryComponent: "App" - }, /*#__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' - } - });" - `); -}); - -it("handles ternary operation returned by function body", () => { - const result = transform( - `const maybeTrue = Math.random() > 0.5; -export default function componentName() { - return (maybeTrue ? '' : ()) -}`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchInlineSnapshot(` - "const maybeTrue = Math.random() > 0.5; - export default function componentName() { - return maybeTrue ? '' : /*#__PURE__*/React.createElement(SubComponent, null); - }" - `); -}); - -it("ignores components with member expressions when in ignoredComponents", () => { - const result = transform( - `import React from 'react'; -import { Tab } from '@headlessui/react'; - -export default function TestComponent() { - return ( -
- - - Tab 1 - Tab 2 - - - Content 1 - Content 2 - - -
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [plugin, { ignoredComponents: ["Tab.Group", "Tab.List", "Tab.Panels", "Tab.Panel"] }], - ], - } - ); - - // The component should be transformed but Tab.* components should not have annotations - expect(result?.code).toContain("React.createElement(Tab.Group"); - expect(result?.code).not.toContain('"data-sentry-element": "Tab.Group"'); - expect(result?.code).toContain("React.createElement(Tab.List"); - expect(result?.code).not.toContain('"data-sentry-element": "Tab.List"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - import { Tab } from '@headlessui/react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, /*#__PURE__*/React.createElement(Tab.Group, null, /*#__PURE__*/React.createElement(Tab.List, null, /*#__PURE__*/React.createElement(Tab, null, "Tab 1"), /*#__PURE__*/React.createElement(Tab, null, "Tab 2")), /*#__PURE__*/React.createElement(Tab.Panels, null, /*#__PURE__*/React.createElement(Tab.Panel, null, "Content 1"), /*#__PURE__*/React.createElement(Tab.Panel, null, "Content 2")))); - }" - `); -}); - -it("handles nested member expressions in component names", () => { - const result = transform( - `import React from 'react'; -import { Components } from 'my-ui-library'; - -export default function TestComponent() { - return ( -
- Click me - Title -
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { ignoredComponents: ["Components.UI.Button"] }]], - } - ); - - // Components.UI.Button should be ignored but Components.UI.Card.Header should be annotated - expect(result?.code).toContain("React.createElement(Components.UI.Button"); - expect(result?.code).not.toContain('"data-sentry-element": "Components.UI.Button"'); - expect(result?.code).toContain("React.createElement(Components.UI.Card.Header"); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - import { Components } from 'my-ui-library'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, /*#__PURE__*/React.createElement(Components.UI.Button, null, "Click me"), /*#__PURE__*/React.createElement(Components.UI.Card.Header, null, "Title")); - }" - `); -}); - -it("Only injects in root html elements", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
-

Title

-

Content

-
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment as F } from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, /*#__PURE__*/React.createElement("h1", null, "Title"), /*#__PURE__*/React.createElement("p", null, "Content"))); - }" - `); -}); - -describe("Fragment Detection", () => { - it("ignores React.Fragment with member expression handling", () => { - const result = transform( - `import React from 'react'; - - export default function TestComponent() { - return ( - -
Content
-
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(React.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content")); - }" - `); - }); - - it("ignores JSX fragments (<>)", () => { - const result = transform( - `export default function TestComponent() { - return ( - <> -
Content in JSX fragment
- More content - - ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(React.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "export default function TestComponent() { - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in JSX fragment"), /*#__PURE__*/React.createElement("span", { - "data-sentry-component": "TestComponent" - }, "More content")); - }" - `); - }); - - it("ignores Fragment imported with alias", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
Content in aliased fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(F"); - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment as F } from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in aliased fragment")); - }" - `); - }); - - it("ignores Fragment assigned to variable", () => { - const result = transform( - `import { Fragment } from 'react'; - -const MyFragment = Fragment; - -export default function TestComponent() { - return ( - -
Content in variable fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(MyFragment"); - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment } from 'react'; - const MyFragment = Fragment; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyFragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in variable fragment")); - }" - `); - }); - - it("ignores Fragment with React namespace alias", () => { - const result = transform( - `import * as MyReact from 'react'; - -export default function TestComponent() { - return ( - -
Content in namespaced fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(MyReact.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "MyReact.Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import * as MyReact from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyReact.Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in namespaced fragment")); - }" - `); - }); - - it("ignores React default import with Fragment", () => { - const result = transform( - `import MyReact from 'react'; - -export default function TestComponent() { - return ( - -
Content in default import fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(MyReact.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "MyReact.Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import MyReact from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyReact.Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in default import fragment")); - }" - `); - }); - - it("ignores multiple fragment patterns in same file", () => { - const result = transform( - `import React, { Fragment } from 'react'; - - const MyFragment = Fragment; - - export default function TestComponent() { - return ( -
- <> -
JSX Fragment content
- - - - Direct Fragment content - - - -

Variable Fragment content

-
- - -

React.Fragment content

-
-
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Fragment } from 'react'; - const MyFragment = Fragment; - export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", null, "JSX Fragment content")), /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("span", null, "Direct Fragment content")), /*#__PURE__*/React.createElement(MyFragment, null, /*#__PURE__*/React.createElement("p", null, "Variable Fragment content")), /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "React.Fragment content"))); - }" - `); - }); - - it("handles complex variable assignment chains", () => { - const result = transform( - `import { Fragment } from 'react'; - - const MyFragment = Fragment; - const AnotherFragment = MyFragment; - - export default function TestComponent() { - return ( - -
Content in chained fragment
-
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "AnotherFragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment } from 'react'; - const MyFragment = Fragment; - const AnotherFragment = MyFragment; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(AnotherFragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in chained fragment")); - }" - `); - }); - - it("works with annotate-fragments option disabled", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
Content
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment as F } from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content")); - }" - `); - }); - - it("works with annotate-fragments option enabled", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
Content
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment as F } from 'react'; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content")); - }" - `); - }); - - it("ignores Fragment from React destructuring", () => { - const result = transform( - `import React from 'react'; - -const { Fragment } = React; - -export default function TestComponent() { - return ( - -
Content in destructured fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - const { - Fragment - } = React; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in destructured fragment")); - }" - `); - }); - - it("ignores Fragment with destructuring alias", () => { - const result = transform( - `import React from 'react'; - -const { Fragment: MyFragment } = React; - -export default function TestComponent() { - return ( - -
Content in aliased destructured fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - const { - Fragment: MyFragment - } = React; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(MyFragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content in aliased destructured fragment")); - }" - `); - }); - - it("ignores Fragment from mixed destructuring", () => { - const result = transform( - `import React from 'react'; - -const { Fragment, createElement, useState } = React; - -export default function TestComponent() { - return ( - -
Content with other destructured items
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - const { - Fragment, - createElement, - useState - } = React; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content with other destructured items")); - }" - `); - }); - - it("handles destructuring from aliased React imports", () => { - const result = transform( - `import MyReact from 'react'; - -const { Fragment } = MyReact; - -export default function TestComponent() { - return ( - -
Content from aliased React destructuring
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import MyReact from 'react'; - const { - Fragment - } = MyReact; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content from aliased React destructuring")); - }" - `); - }); - - it("handles destructuring from namespace imports", () => { - const result = transform( - `import * as ReactLib from 'react'; - -const { Fragment: F } = ReactLib; - -export default function TestComponent() { - return ( - -
Content from namespace destructuring
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchInlineSnapshot(` - "import * as ReactLib from 'react'; - const { - Fragment: F - } = ReactLib; - export default function TestComponent() { - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "Content from namespace destructuring")); - }" - `); - }); - - it("handles multiple destructuring patterns in one file", () => { - const result = transform( - `import React from 'react'; -import * as MyReact from 'react'; - -const { Fragment } = React; -const { Fragment: AliasedFrag } = MyReact; - -export default function TestComponent() { - return ( -
- - Regular destructured - - - -

Aliased destructured

-
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "AliasedFrag"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React from 'react'; - import * as MyReact from 'react'; - const { - Fragment - } = React; - const { - Fragment: AliasedFrag - } = MyReact; - export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("span", null, "Regular destructured")), /*#__PURE__*/React.createElement(AliasedFrag, null, /*#__PURE__*/React.createElement("p", null, "Aliased destructured"))); - }" - `); - }); - - it("combines all fragment patterns correctly", () => { - const result = transform( - `import React, { Fragment as ImportedF } from 'react'; - import * as MyReact from 'react'; - - const { Fragment: DestructuredF } = React; - const { Fragment } = MyReact; - const AssignedF = Fragment; // ← This uses the destructured Fragment from MyReact - - export default function TestComponent() { - return ( -
- {/* JSX Fragment */} - <> - JSX Fragment content - - - {/* Imported alias */} - - Imported alias content - - - {/* Destructured */} - - Destructured content - - - {/* Destructured from namespace */} - - Namespace destructured content - - - {/* Variable assigned */} - - Variable assigned content - - - {/* React.Fragment */} - - React.Fragment content - - - {/* Namespace Fragment */} - - Namespace Fragment content - -
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "ImportedF"'); - expect(result?.code).not.toContain('"data-sentry-element": "DestructuredF"'); - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "AssignedF"'); - expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "MyReact.Fragment"'); - expect(result?.code).toMatchInlineSnapshot(` - "import React, { Fragment as ImportedF } from 'react'; - import * as MyReact from 'react'; - const { - Fragment: DestructuredF - } = React; - const { - Fragment - } = MyReact; - const AssignedF = Fragment; // ← This uses the destructured Fragment from MyReact - - export default function TestComponent() { - return /*#__PURE__*/React.createElement("div", { - className: "container", - "data-sentry-component": "TestComponent" - }, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", null, "JSX Fragment content")), /*#__PURE__*/React.createElement(ImportedF, null, /*#__PURE__*/React.createElement("span", null, "Imported alias content")), /*#__PURE__*/React.createElement(DestructuredF, null, /*#__PURE__*/React.createElement("span", null, "Destructured content")), /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement("span", null, "Namespace destructured content")), /*#__PURE__*/React.createElement(AssignedF, null, /*#__PURE__*/React.createElement("span", null, "Variable assigned content")), /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", null, "React.Fragment content")), /*#__PURE__*/React.createElement(MyReact.Fragment, null, /*#__PURE__*/React.createElement("span", null, "Namespace Fragment content"))); - }" - `); - }); - - it("handles Fragment aliased correctly when used by other non-Fragment components in a different scope", () => { - const result = transform( - `import { Fragment as OriginalF } from 'react'; -import { OtherComponent } from 'some-library'; - -function TestComponent() { - const F = OriginalF; - - // Use Fragment alias - should be ignored - return ( - -
This should NOT have data-sentry-element (Fragment)
-
- ); -} - -function AnotherComponent() { - // Different component with same alias name in different function scope - const F = OtherComponent; - - return ( - -
This SHOULD have data-sentry-element (not Fragment)
-
- ); -} -`, - { - filename: "/variable-assignment-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchInlineSnapshot(` - "import { Fragment as OriginalF } from 'react'; - import { OtherComponent } from 'some-library'; - function TestComponent() { - const F = OriginalF; - - // Use Fragment alias - should be ignored - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "TestComponent" - }, "This should NOT have data-sentry-element (Fragment)")); - } - function AnotherComponent() { - // Different component with same alias name in different function scope - const F = OtherComponent; - return /*#__PURE__*/React.createElement(F, null, /*#__PURE__*/React.createElement("div", { - "data-sentry-component": "AnotherComponent" - }, "This SHOULD have data-sentry-element (not Fragment)")); - }" - `); - }); -}); diff --git a/packages/bundler-plugins/test/babel-plugin/sentry-label.test.ts b/packages/bundler-plugins/test/babel-plugin/sentry-label.test.ts deleted file mode 100644 index 822eb95f..00000000 --- a/packages/bundler-plugins/test/babel-plugin/sentry-label.test.ts +++ /dev/null @@ -1,1009 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { BabelFileResult } from "@babel/core"; -import { transform } from "@babel/core"; -import plugin from "../../src/babel-plugin/index"; - -function transformWith(code: string, opts: Record = {}): BabelFileResult | null { - return transform(code, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { autoInjectSentryLabel: true, ...opts }]], - }); -} - -function transformWithout( - code: string, - opts: Record = {} -): BabelFileResult | null { - return transform(code, { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, opts]], - }); -} - -describe("autoInjectSentryLabel", () => { - describe("opt-in behavior", () => { - it("does not inject sentry-label when autoInjectSentryLabel is not set", () => { - const result = transformWithout(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("does not inject sentry-label when autoInjectSentryLabel is false", () => { - const result = transformWithout( - ` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `, - { autoInjectSentryLabel: false } - ); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("injects sentry-label when autoInjectSentryLabel is true", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello"'); - }); - }); - - describe("basic static text extraction", () => { - it("extracts text from a Text child", () => { - const result = transformWith(` - import React from 'react'; - import { Text, TouchableOpacity } from 'react-native'; - - export default function SaveButton() { - return ( - - Save workout - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Save workout"'); - }); - - it("extracts text from a nested Text within a View", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View, TouchableOpacity } from 'react-native'; - - export default function Card() { - return ( - - - Details - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Details"'); - }); - - it("works with arrow function components", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - const MyButton = () => ( - - Press me - - ); - `); - expect(result?.code).toContain('"sentry-label": "Press me"'); - }); - - it("works with class components", () => { - const result = transformWith(` - import React, { Component } from 'react'; - import { Text, View } from 'react-native'; - - class MyButton extends Component { - render() { - return ( - - Click here - - ); - } - } - `); - expect(result?.code).toContain('"sentry-label": "Click here"'); - }); - }); - - describe("multiple text children", () => { - it("joins text from multiple Text children with space", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function AddToCart() { - return ( - - Add - to cart - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Add to cart"'); - }); - - it("joins text from multiple nested Text children", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View, TouchableOpacity } from 'react-native'; - - export default function Header() { - return ( - - - Welcome - back - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Welcome back"'); - }); - }); - - describe("skip conditions", () => { - it("skips when sentry-label already exists", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Auto text - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Custom label"'); - expect(result?.code).not.toContain('"sentry-label": "Auto text"'); - }); - - it("skips dynamic expression children", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - {variable} - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips when Text child has a function call expression", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - {t('key')} - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips when Text child has a template literal", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - {\`hello \${name}\`} - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips when text is empty or whitespace only", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips when no Text children exist", () => { - const result = transformWith(` - import React from 'react'; - import { View, Image } from 'react-native'; - - export default function MyComponent() { - return ( - - - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips when expression container is at root level", () => { - const result = transformWith(` - import React from 'react'; - import { View } from 'react-native'; - - export default function MyComponent() { - return ( - - {someContent} - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - }); - - describe("truncation", () => { - it("truncates text longer than 64 characters with ...", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - This is an extremely long text that definitely exceeds the sixty-four character limit - - ); - } - `); - const match = result?.code?.match(/"sentry-label": "([^"]+)"/); - expect(match).toBeTruthy(); - const label = match?.[1] ?? ""; - expect(label.length).toBe(64); - expect(label.endsWith("...")).toBe(true); - expect(label).toBe("This is an extremely long text that definitely exceeds the si..."); - }); - - it("does not truncate text at exactly 64 characters", () => { - // 64 chars exactly - const text64 = "A".repeat(64); - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - ${text64} - - ); - } - `); - expect(result?.code).toContain(`"sentry-label": "${text64}"`); - }); - }); - - describe("depth limit", () => { - it("extracts text at depth 1 (direct child)", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Direct child - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Direct child"'); - }); - - it("extracts text at depth 2 (nested in one wrapper)", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - - Nested once - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Nested once"'); - }); - - it("extracts text at depth 3 (nested in two wrappers)", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - - - Nested twice - - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Nested twice"'); - }); - - it("does not extract text beyond depth limit", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - - - - Too deep - - - - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("does not count fragments toward depth limit", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - - - <> - Still found - - - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Still found"'); - }); - }); - - describe("text component names", () => { - it("recognizes lowercase text component", () => { - const result = transformWith(` - import React from 'react'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello"'); - }); - - it("supports custom text component names via option", () => { - const result = transformWith( - ` - import React from 'react'; - - export default function MyComponent() { - return ( - - - - ); - } - `, - { autoInjectSentryLabel: { textComponentNames: ["Label", "Text"] } } - ); - expect(result?.code).toContain('"sentry-label": "Custom text"'); - }); - - it("does not extract from non-text components by default", () => { - const result = transformWith(` - import React from 'react'; - import { View, Button } from 'react-native'; - - export default function MyComponent() { - return ( - - - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - }); - - describe("nested text components (RN inline styling)", () => { - it("extracts text from nested Text inside Text", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello world - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello world"'); - }); - - it("extracts text from deeply nested inline Text", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Press Save now to continue - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Press Save now to continue"'); - }); - - it("bails out when nested Text contains dynamic content", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello {name} - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips non-text elements inside Text without bailing out", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello world - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello world"'); - }); - - it("extracts text from fragment children inside Text", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello <>World more - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello World more"'); - }); - - it("handles Text wrapping only a non-text element", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - hello - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - }); - - describe("web compatibility", () => { - it("uses hyphenated sentry-label attribute", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `); - expect(result?.code).toContain('"sentry-label"'); - expect(result?.code).not.toContain("sentryLabel"); - }); - - it("uses sentry-label in native mode too", () => { - const result = transformWith( - ` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `, - { native: true } - ); - expect(result?.code).toContain('"sentry-label": "Hello"'); - }); - }); - - describe("fragment handling", () => { - it("injects on first element child when root is a fragment", () => { - const result = transformWith(` - import React from 'react'; - import { Text, TouchableOpacity } from 'react-native'; - - export default function MyComponent() { - return ( - <> - - Hello - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello"'); - }); - - it("extracts text only from the target element, not sibling fragment children", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - <> - A - B - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "A"'); - expect(result?.code).not.toContain('"sentry-label": "A B"'); - }); - - it("skips root fragment when it has no element children", () => { - const result = transformWith(` - import React from 'react'; - - export default function MyComponent() { - return ( - <> - Just text - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("skips root fragment when first child already has sentry-label", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - <> - - Auto text - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Manual"'); - expect(result?.code).not.toContain('"sentry-label": "Auto text"'); - }); - - it("traverses through fragment children to find text", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - <> - Fragment text - - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Fragment text"'); - }); - }); - - describe("edge cases", () => { - it("trims whitespace from extracted text", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello world - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello world"'); - }); - - it("normalizes double spaces when joining text from multiple components", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - world - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello world"'); - expect(result?.code).not.toContain("Hello world"); - }); - - it("collapses internal whitespace", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello world - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Hello world"'); - }); - - it("still adds other sentry attributes alongside sentry-label", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello - - ); - } - `); - expect(result?.code).toContain("data-sentry-component"); - expect(result?.code).toContain("data-sentry-source-file"); - expect(result?.code).toContain('"sentry-label": "Hello"'); - }); - - it("handles mixed static and dynamic children — skips all when dynamic present", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Static - {dynamicContent} - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("respects ignoredComponents — does not inject sentry-label", () => { - const result = transformWith( - ` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function IgnoredComp() { - return ( - - Should not label - - ); - } - `, - { ignoredComponents: ["IgnoredComp"] } - ); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("respects ignoredComponents matching the element name", () => { - const result = transformWith( - ` - import React from 'react'; - import { Text } from 'react-native'; - import { CustomCard } from './components'; - - export default function MyComponent() { - return ( - - Card text - - ); - } - `, - { ignoredComponents: ["CustomCard"] } - ); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("extracts text from JSXText inside a fragment child of root", () => { - const result = transformWith(` - import React from 'react'; - - export default function MyComponent() { - return ; - } - `); - expect(result?.code).toContain('"sentry-label": "Click me"'); - }); - - it("bails out when non-text element inside Text contains dynamic content", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - Hello {name} world - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("handles direct JSXText on the root element", () => { - const result = transformWith(` - import React from 'react'; - - export default function MyComponent() { - return ; - } - `); - expect(result?.code).toContain('"sentry-label": "Click me"'); - }); - - it("bails out entirely when dynamic content is nested inside a non-text wrapper", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return ( - - - {dynamic} - - Static - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("does not match member-expression text components against simple name", () => { - const result = transformWith(` - import React from 'react'; - import { View } from 'react-native'; - import MyLib from 'my-lib'; - - export default function MyComponent() { - return ( - - Not matched - - ); - } - `); - expect(result?.code).not.toContain("sentry-label"); - }); - - it("matches member-expression text components when configured", () => { - const result = transformWith( - ` - import React from 'react'; - import { View } from 'react-native'; - import MyLib from 'my-lib'; - - export default function MyComponent() { - return ( - - Matched - - ); - } - `, - { autoInjectSentryLabel: { textComponentNames: ["Text", "MyLib.Text"] } } - ); - expect(result?.code).toContain('"sentry-label": "Matched"'); - }); - }); - - describe("multiple components in one file", () => { - it("injects independent labels on each component", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - function SaveButton() { - return ( - - Save - - ); - } - - function CancelButton() { - return ( - - Cancel - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Save"'); - expect(result?.code).toContain('"sentry-label": "Cancel"'); - }); - - it("only injects on components that have text, not on others", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View, Image } from 'react-native'; - - function IconButton() { - return ( - - - - ); - } - - function TextButton() { - return ( - - Click - - ); - } - `); - expect(result?.code).toContain('"sentry-label": "Click"'); - const matches = result?.code?.match(/"sentry-label"/g); - expect(matches?.length).toBe(1); - }); - }); - - describe("ternary returns", () => { - it("injects labels on both branches of a ternary", () => { - const result = transformWith(` - import React from 'react'; - import { Text, View } from 'react-native'; - - export default function MyComponent() { - return condition - ? Yes - : No; - } - `); - expect(result?.code).toContain('"sentry-label": "Yes"'); - expect(result?.code).toContain('"sentry-label": "No"'); - }); - }); -}); diff --git a/packages/bundler-plugins/test/babel-plugin/test-plugin.test.ts b/packages/bundler-plugins/test/babel-plugin/test-plugin.test.ts deleted file mode 100644 index 84d97796..00000000 --- a/packages/bundler-plugins/test/babel-plugin/test-plugin.test.ts +++ /dev/null @@ -1,1783 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2020 Engineering at FullStory - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { describe, it, expect } from "vitest"; -import { transform } from "@babel/core"; -import plugin from "../../src/babel-plugin/index"; - -const BananasPizzaAppStandardInput = `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 ; - } -} - -class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { text: '' }; - } - - render() { - return - this.setState({ text })} value={this.state.text} /> - - {this.state.text.split(' ').map(word => word && '🍕').join(' ')} - - ; - } -} - -export default function App() { - return - FullStory ReactNative testing app - - - ; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } -});`; - -const BananasStandardInput = `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 ; - } -}`; - -it("unknown-element snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return

A

; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -class componentName extends Component { - render() { - return A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return <>A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-annotate-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return <>A; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-annotate-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return -

Hello world

-
; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-annotate-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return <> -

Hello world

- ; - } -} - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( - -

Hello world

-
-); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( - <> -

Hello world

- -); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( - -

Hello world

-
-); - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-annotate-trivial-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( - Hello world -); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-annotate-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( - -

Hello world

-
-); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-annotate-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( - -

Hello world

-
-); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-annotate-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( - <> -

Hello world

- -); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-annotate-fragment-once snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( - -

Hello world

-

Hola Sol

-
-); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn-annotate-fragment-no-whitespace snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => ( -

Hello world

Hola Sol

-); - -export default componentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return
-

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("option-attribute snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return
-

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class componentName extends Component { - render() { - return
-

Hello world

-
; - } -} - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction-annotate-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -function SubComponent() { - return Sub; -} - -const componentName = () => { - return - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction-annotate-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return Sub; -} - -const componentName = () => { - return - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction-annotate-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return <>Sub; -} - -const componentName = () => { - return <> - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -function SubComponent() { - return Sub; -} - -const componentName = () => { - return - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return Sub; -} - -const componentName = () => { - return - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return <>Sub; -} - -const componentName = () => { - return <> - - ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-noreturn snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => ( -
-

Hello world

-
-); - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("tags snapshot matches", () => { - const result = transform( - `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 ; - } -} - -class PizzaTranslator extends Component { - constructor(props) { - super(props); - this.state = { text: '' }; - } - - render() { - return - this.setState({ text })} value={this.state.text} /> - - {this.state.text.split(' ').map(word => word && '🍕').join(' ')} - - ; - } -} - -export default function App() { - return - FullStory ReactNative testing app - - - ; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } -}); -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("option-format snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return
-

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("pureComponent-fragment snapshot matches", () => { - const result = transform( - `import React, { Fragment } from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return -

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("pureComponent-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return <> -

Hello world

- ; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("pureComponent-react-fragment snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return -

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("rawfunction snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -function SubComponent() { - return
Sub
; -} - -const componentName = () => { - return
- -
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => { - return -

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React from 'react'; - -const componentName = () => { - return <> -

Hello world

- ; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return -

Hello world

-
; -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("nonJSX snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -class TestClass extends Component { - test() { - return true; - } -} - -export default TestClass; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-anonymous-fragment snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -const componentName = () => { - return (() => -

Hello world

-
)(); -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-anonymous-shorthand-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return (() => <> -

Hello world

- )(); -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("arrow-anonymous-react-fragment snapshot matches", () => { - const result = transform( - `import React, { Component } from 'react'; - -const componentName = () => { - return (() => -

Hello world

-
)(); -}; - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("pure snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return
-

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("component-fragment-native snapshot matches", () => { - const result = transform( - `import React, { Component, Fragment } from 'react'; - -class componentName extends Component { - render() { - return A; - } -} - -export default componentName; -`, - { - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("pure-native snapshot matches", () => { - const result = transform( - `import React from 'react'; - -class PureComponentName extends React.PureComponent { - render() { - return
-

Hello world

-
; - } -} - -export default PureComponentName; -`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { native: true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -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("skips components marked in ignoredComponents", () => { - const result = transform(BananasPizzaAppStandardInput, { - filename: "/filename-test.js", - presets: ["@babel/preset-react"], - 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'; - 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, - 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, { - dataSentryElement: "PizzaTranslator", - dataSentrySourceFile: "filename-test.js" - })); - } - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'stretch', - backgroundColor: '#222', - alignItems: 'center', - justifyContent: 'center' - } - });" - `); -}); - -it("handles ternary operation returned by function body", () => { - const result = transform( - `const maybeTrue = Math.random() > 0.5; -export default function componentName() { - return (maybeTrue ? '' : ()) -}`, - { - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - expect(result?.code).toMatchSnapshot(); -}); - -it("ignores components with member expressions when in ignoredComponents", () => { - const result = transform( - `import React from 'react'; -import { Tab } from '@headlessui/react'; - -export default function TestComponent() { - return ( -
- - - Tab 1 - Tab 2 - - - Content 1 - Content 2 - - -
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [ - [plugin, { ignoredComponents: ["Tab.Group", "Tab.List", "Tab.Panels", "Tab.Panel"] }], - ], - } - ); - - // The component should be transformed but Tab.* components should not have annotations - expect(result?.code).toContain("React.createElement(Tab.Group"); - expect(result?.code).not.toContain('"data-sentry-element": "Tab.Group"'); - expect(result?.code).toContain("React.createElement(Tab.List"); - expect(result?.code).not.toContain('"data-sentry-element": "Tab.List"'); - expect(result?.code).toMatchSnapshot(); -}); - -it("handles nested member expressions in component names", () => { - const result = transform( - `import React from 'react'; -import { Components } from 'my-ui-library'; - -export default function TestComponent() { - return ( -
- Click me - Title -
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { ignoredComponents: ["Components.UI.Button"] }]], - } - ); - - // Components.UI.Button should be ignored but Components.UI.Card.Header should be annotated - expect(result?.code).toContain("React.createElement(Components.UI.Button"); - expect(result?.code).not.toContain('"data-sentry-element": "Components.UI.Button"'); - expect(result?.code).toContain("React.createElement(Components.UI.Card.Header"); - expect(result?.code).toContain('"data-sentry-element": "Components.UI.Card.Header"'); - expect(result?.code).toMatchSnapshot(); -}); - -describe("Fragment Detection", () => { - it("ignores React.Fragment with member expression handling", () => { - const result = transform( - `import React from 'react'; - - export default function TestComponent() { - return ( - -
Content
-
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(React.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores JSX fragments (<>)", () => { - const result = transform( - `export default function TestComponent() { - return ( - <> -
Content in JSX fragment
- More content - - ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(React.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores Fragment imported with alias", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
Content in aliased fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(F"); - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores Fragment assigned to variable", () => { - const result = transform( - `import { Fragment } from 'react'; - -const MyFragment = Fragment; - -export default function TestComponent() { - return ( - -
Content in variable fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(MyFragment"); - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores Fragment with React namespace alias", () => { - const result = transform( - `import * as MyReact from 'react'; - -export default function TestComponent() { - return ( - -
Content in namespaced fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(MyReact.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "MyReact.Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores React default import with Fragment", () => { - const result = transform( - `import MyReact from 'react'; - -export default function TestComponent() { - return ( - -
Content in default import fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).toContain("React.createElement(MyReact.Fragment"); - expect(result?.code).not.toContain('"data-sentry-element": "MyReact.Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores multiple fragment patterns in same file", () => { - const result = transform( - `import React, { Fragment } from 'react'; - - const MyFragment = Fragment; - - export default function TestComponent() { - return ( -
- <> -
JSX Fragment content
- - - - Direct Fragment content - - - -

Variable Fragment content

-
- - -

React.Fragment content

-
-
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("handles complex variable assignment chains", () => { - const result = transform( - `import { Fragment } from 'react'; - - const MyFragment = Fragment; - const AnotherFragment = MyFragment; - - export default function TestComponent() { - return ( - -
Content in chained fragment
-
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "AnotherFragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("works with annotate-fragments option disabled", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
Content
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": false }]], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("works with annotate-fragments option enabled", () => { - const result = transform( - `import { Fragment as F } from 'react'; - -export default function TestComponent() { - return ( - -
Content
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [[plugin, { "annotate-fragments": true }]], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores Fragment from React destructuring", () => { - const result = transform( - `import React from 'react'; - -const { Fragment } = React; - -export default function TestComponent() { - return ( - -
Content in destructured fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores Fragment with destructuring alias", () => { - const result = transform( - `import React from 'react'; - -const { Fragment: MyFragment } = React; - -export default function TestComponent() { - return ( - -
Content in aliased destructured fragment
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "MyFragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("ignores Fragment from mixed destructuring", () => { - const result = transform( - `import React from 'react'; - -const { Fragment, createElement, useState } = React; - -export default function TestComponent() { - return ( - -
Content with other destructured items
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("handles destructuring from aliased React imports", () => { - const result = transform( - `import MyReact from 'react'; - -const { Fragment } = MyReact; - -export default function TestComponent() { - return ( - -
Content from aliased React destructuring
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("handles destructuring from namespace imports", () => { - const result = transform( - `import * as ReactLib from 'react'; - -const { Fragment: F } = ReactLib; - -export default function TestComponent() { - return ( - -
Content from namespace destructuring
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("handles multiple destructuring patterns in one file", () => { - const result = transform( - `import React from 'react'; -import * as MyReact from 'react'; - -const { Fragment } = React; -const { Fragment: AliasedFrag } = MyReact; - -export default function TestComponent() { - return ( -
- - Regular destructured - - - -

Aliased destructured

-
-
- ); -}`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "AliasedFrag"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("combines all fragment patterns correctly", () => { - const result = transform( - `import React, { Fragment as ImportedF } from 'react'; - import * as MyReact from 'react'; - - const { Fragment: DestructuredF } = React; - const { Fragment } = MyReact; - const AssignedF = Fragment; // ← This uses the destructured Fragment from MyReact - - export default function TestComponent() { - return ( -
- {/* JSX Fragment */} - <> - JSX Fragment content - - - {/* Imported alias */} - - Imported alias content - - - {/* Destructured */} - - Destructured content - - - {/* Destructured from namespace */} - - Namespace destructured content - - - {/* Variable assigned */} - - Variable assigned content - - - {/* React.Fragment */} - - React.Fragment content - - - {/* Namespace Fragment */} - - Namespace Fragment content - -
- ); - }`, - { - filename: "/filename-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "ImportedF"'); - expect(result?.code).not.toContain('"data-sentry-element": "DestructuredF"'); - expect(result?.code).not.toContain('"data-sentry-element": "Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "AssignedF"'); - expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); - expect(result?.code).not.toContain('"data-sentry-element": "MyReact.Fragment"'); - expect(result?.code).toMatchSnapshot(); - }); - - it("handles Fragment aliased correctly when used by other non-Fragment components in a different scope", () => { - const result = transform( - `import { Fragment as OriginalF } from 'react'; -import { OtherComponent } from 'some-library'; - -function TestComponent() { - const F = OriginalF; - - // Use Fragment alias - should be ignored - return ( - -
This should NOT have data-sentry-element (Fragment)
-
- ); -} - -function AnotherComponent() { - // Different component with same alias name in different function scope - const F = OtherComponent; - - return ( - -
This SHOULD have data-sentry-element (not Fragment)
-
- ); -} -`, - { - filename: "/variable-assignment-test.js", - configFile: false, - presets: ["@babel/preset-react"], - plugins: [plugin], - } - ); - - expect(result?.code).not.toContain('"data-sentry-element": "F"'); - expect(result?.code).toMatchSnapshot(); - }); -}); diff --git a/packages/bundler-plugins/test/core/__snapshots__/utils.test.ts.snap b/packages/bundler-plugins/test/core/__snapshots__/utils.test.ts.snap deleted file mode 100644 index e35bb4f5..00000000 --- a/packages/bundler-plugins/test/core/__snapshots__/utils.test.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`generateModuleMetadataInjectorCode > generates code with empty metadata object 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n generates code with metadata object 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n generates code with release 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"1.2.3"};}catch(e){}}();"`; - -exports[`generateReleaseInjectorCode > generates code with release and build information 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"1.2.3"};e.SENTRY_BUILD_INFO={"deps":["myDep","rollup"],"depsVersions":{"rollup":3},"nodeVersion":22};}catch(e){}}();"`; diff --git a/packages/bundler-plugins/test/core/build-plugin-manager.test.ts b/packages/bundler-plugins/test/core/build-plugin-manager.test.ts deleted file mode 100644 index 49717c60..00000000 --- a/packages/bundler-plugins/test/core/build-plugin-manager.test.ts +++ /dev/null @@ -1,815 +0,0 @@ -import { - createSentryBuildPluginManager, - _resetDeployedReleasesForTesting, -} from "../../src/core/build-plugin-manager"; -import fs from "fs"; -import { globFiles } from "../../src/core/glob"; -import { prepareBundleForDebugIdUpload } from "../../src/core/debug-id-upload"; -import type { MockedFunction } from "vitest"; -import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; - -const { mockCliExecute, mockCliUploadSourceMaps, mockCliNewDeploy, mockCliConstructor } = - vi.hoisted(() => ({ - mockCliExecute: vi.fn(), - mockCliUploadSourceMaps: vi.fn(), - mockCliNewDeploy: vi.fn(), - mockCliConstructor: vi.fn(), - })); - -vi.mock("@sentry/cli", () => ({ - default: class { - constructor(...args: unknown[]) { - mockCliConstructor(...args); - } - execute = mockCliExecute; - releases = { - uploadSourceMaps: mockCliUploadSourceMaps, - new: vi.fn(), - finalize: vi.fn(), - setCommits: vi.fn(), - newDeploy: mockCliNewDeploy, - }; - }, -})); - -vi.mock("../../src/core/sentry/telemetry", async () => ({ - ...(await vi.importActual("../../src/core/sentry/telemetry")), - safeFlushTelemetry: vi.fn(), -})); - -vi.mock("@sentry/core", async () => ({ - ...(await vi.importActual("@sentry/core")), - startSpan: vi.fn((options: unknown, callback: () => unknown) => callback()), -})); - -vi.mock("../../src/core/glob"); -vi.mock("../../src/core/debug-id-upload"); - -const mockGlobFiles = globFiles as MockedFunction; -const mockPrepareBundleForDebugIdUpload = - prepareBundleForDebugIdUpload as unknown as MockedFunction; - -describe("createSentryBuildPluginManager", () => { - beforeEach(() => { - vi.clearAllMocks(); - // Clean up environment variables - delete process.env["SENTRY_LOG_LEVEL"]; - }); - - describe("debug option", () => { - it("should set SENTRY_LOG_LEVEL environment variable when debug is true", () => { - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: true, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("debug"); - }); - - it("should NOT override existing SENTRY_LOG_LEVEL even when debug is true", () => { - // User explicitly set SENTRY_LOG_LEVEL to "info" - process.env["SENTRY_LOG_LEVEL"] = "info"; - - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: true, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - // Should respect the user's explicit setting - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("info"); - }); - - it("should not set SENTRY_LOG_LEVEL environment variable when debug is false", () => { - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: false, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(process.env["SENTRY_LOG_LEVEL"]).toBeUndefined(); - }); - - it("should not set SENTRY_LOG_LEVEL environment variable when debug is not specified", () => { - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(process.env["SENTRY_LOG_LEVEL"]).toBeUndefined(); - }); - - it("should have SENTRY_LOG_LEVEL set when CLI operations are performed with debug enabled", async () => { - mockCliExecute.mockImplementation(() => { - // Verify the environment variable is set at the time the CLI is called - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("debug"); - return Promise.resolve(undefined); - }); - - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: true, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - // Verify it's set immediately after creation - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("debug"); - - // Perform a CLI operation and verify the env var is still set - await buildPluginManager.injectDebugIds(["/path/to/bundle"]); - - expect(mockCliExecute).toHaveBeenCalled(); - }); - - it("should have SENTRY_LOG_LEVEL set during error scenarios with debug enabled", async () => { - // Simulate CLI error - mockCliExecute.mockImplementation(() => { - // Verify the environment variable is set even when CLI encounters an error - // This ensures the CLI won't emit the "Add --log-level=debug" warning - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("debug"); - return Promise.reject(new Error("CLI error")); - }); - - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: true, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - // Verify it's set before the error - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("debug"); - - // Perform a CLI operation that will fail - await buildPluginManager.injectDebugIds(["/path/to/bundle"]); - - // The error should have been caught, but env var should still be set - expect(process.env["SENTRY_LOG_LEVEL"]).toBe("debug"); - }); - - it("should NOT have SENTRY_LOG_LEVEL set during error scenarios when debug is disabled", async () => { - // Simulate CLI error - mockCliExecute.mockImplementation(() => { - // Verify the environment variable is NOT set - // In this case, the CLI WOULD emit the "Add --log-level=debug" warning - expect(process.env["SENTRY_LOG_LEVEL"]).toBeUndefined(); - return Promise.reject(new Error("CLI error")); - }); - - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: false, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - // Verify it's not set - expect(process.env["SENTRY_LOG_LEVEL"]).toBeUndefined(); - - // Perform a CLI operation that will fail - await buildPluginManager.injectDebugIds(["/path/to/bundle"]); - - // The error should have been caught, and env var should still not be set - expect(process.env["SENTRY_LOG_LEVEL"]).toBeUndefined(); - }); - }); - - describe("when disabled", () => { - it("initializes a no-op build plugin manager", () => { - const buildPluginManager = createSentryBuildPluginManager( - { - disable: true, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(buildPluginManager).toBeDefined(); - expect(buildPluginManager.logger).toBeDefined(); - expect(buildPluginManager.normalizedOptions.disable).toBe(true); - }); - - it("does not log anything to the console", () => { - const logSpy = vi.spyOn(console, "log"); - const infoSpy = vi.spyOn(console, "info"); - const debugSpy = vi.spyOn(console, "debug"); - const warnSpy = vi.spyOn(console, "warn"); - const errorSpy = vi.spyOn(console, "error"); - - createSentryBuildPluginManager( - { - disable: true, - release: { - deploy: { - // An empty string triggers a validation error (but satisfies the type checker) - env: "", - }, - }, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(logSpy).not.toHaveBeenCalled(); - expect(infoSpy).not.toHaveBeenCalled(); - expect(debugSpy).not.toHaveBeenCalled(); - expect(warnSpy).not.toHaveBeenCalled(); - expect(errorSpy).not.toHaveBeenCalled(); - }); - }); - - describe("uploadSourcemaps", () => { - it("uploads in-place when prepareArtifacts is false", async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); - - const manager = createSentryBuildPluginManager( - { - authToken: "t", - org: "o", - project: "p", - release: { name: "some-release-name", dist: "1" }, - sourcemaps: { assets: ["/app/dist/**/*"] }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.uploadSourcemaps(["/unused"], { prepareArtifacts: false }); - - expect(mockCliUploadSourceMaps).toHaveBeenCalledTimes(1); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - "some-release-name", - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - include: expect.arrayContaining([ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // User-provided assets should be passed directly to CLI (no globbing) - paths: ["/app/dist/**/*"], - rewrite: true, - dist: "1", - }), - ]), - live: "rejectOnError", - }) - ); - // Should not glob when prepareArtifacts is false - expect(mockGlobFiles).not.toHaveBeenCalled(); - expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); - }); - - it("uploads build artifact paths when prepareArtifacts is false and no assets provided", async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); - - const manager = createSentryBuildPluginManager( - { - authToken: "t", - org: "o", - project: "p", - release: { name: "some-release-name", dist: "1" }, - // No assets provided - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.uploadSourcemaps([".next", "dist"], { prepareArtifacts: false }); - - expect(mockCliUploadSourceMaps).toHaveBeenCalledTimes(1); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - "some-release-name", - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - include: expect.arrayContaining([ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // Should use buildArtifactPaths directly - paths: [".next", "dist"], - rewrite: true, - dist: "1", - }), - ]), - live: "rejectOnError", - }) - ); - expect(mockGlobFiles).not.toHaveBeenCalled(); - expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); - }); - - it("exits early when assets is an empty array", async () => { - const manager = createSentryBuildPluginManager( - { - authToken: "t", - org: "o", - project: "p", - release: { name: "some-release-name", dist: "1" }, - sourcemaps: { assets: [] }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.uploadSourcemaps([".next"], { prepareArtifacts: false }); - - expect(mockCliUploadSourceMaps).not.toHaveBeenCalled(); - expect(mockGlobFiles).not.toHaveBeenCalled(); - expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); - }); - - it("exits early when assets is an empty array even for default mode", async () => { - const manager = createSentryBuildPluginManager( - { - authToken: "t", - org: "o", - project: "p", - release: { name: "some-release-name", dist: "1" }, - sourcemaps: { assets: [] }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.uploadSourcemaps([".next"]); - - expect(mockCliUploadSourceMaps).not.toHaveBeenCalled(); - expect(mockGlobFiles).not.toHaveBeenCalled(); - expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); - }); - - it("prepares into temp folder and uploads when prepareArtifacts is true (default)", async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); - - mockGlobFiles.mockResolvedValue([ - "/app/dist/a.js", - "/app/dist/a.js.map", - "/app/dist/other.txt", - ]); - - vi.spyOn(fs.promises, "mkdtemp").mockResolvedValue("/tmp/sentry-upload-xyz"); - vi.spyOn(fs.promises, "readdir").mockResolvedValue(["a.js", "a.js.map"] as never); - vi.spyOn(fs.promises, "stat").mockResolvedValue({ size: 10 } as fs.Stats); - vi.spyOn(fs.promises, "rm").mockResolvedValue(undefined as never); - - mockPrepareBundleForDebugIdUpload.mockResolvedValue(undefined); - - const manager = createSentryBuildPluginManager( - { - authToken: "t", - org: "o", - project: "p", - release: { name: "some-release-name", dist: "1" }, - sourcemaps: { assets: ["/app/dist/**/*"] }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.uploadSourcemaps(["/unused"]); - - // Should call prepare for each JS chunk discovered by glob - expect(mockPrepareBundleForDebugIdUpload).toHaveBeenCalled(); - // Should upload from temp folder - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith("some-release-name", { - include: [{ paths: ["/tmp/sentry-upload-xyz"], rewrite: false, dist: "1" }], - projects: ["p"], - live: "rejectOnError", - }); - }); - }); - - describe("injectDebugIds", () => { - it("should call CLI with correct sourcemaps inject command", async () => { - mockCliExecute.mockResolvedValue(undefined); - - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - const buildArtifactPaths = ["/path/to/1", "/path/to/2"]; - await buildPluginManager.injectDebugIds(buildArtifactPaths); - - expect(mockCliExecute).toHaveBeenCalledWith( - ["sourcemaps", "inject", "--ignore", "node_modules", "/path/to/1", "/path/to/2"], - false - ); - }); - - it('should pass "rejectOnError" flag when options.debug is true', async () => { - mockCliExecute.mockResolvedValue(undefined); - - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - debug: true, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - const buildArtifactPaths = ["/path/to/bundle"]; - await buildPluginManager.injectDebugIds(buildArtifactPaths); - - expect(mockCliExecute).toHaveBeenCalledWith( - ["sourcemaps", "inject", "--ignore", "node_modules", "/path/to/bundle"], - "rejectOnError" - ); - }); - }); - - describe("uploadSourcemaps with multiple projects", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockGlobFiles.mockResolvedValue(["/path/to/bundle.js"]); - mockPrepareBundleForDebugIdUpload.mockResolvedValue(undefined); - mockCliUploadSourceMaps.mockResolvedValue(undefined); - - // Mock fs operations needed for temp folder upload path - vi.spyOn(fs.promises, "mkdtemp").mockResolvedValue("/tmp/sentry-test"); - vi.spyOn(fs.promises, "readdir").mockResolvedValue([]); - vi.spyOn(fs.promises, "stat").mockResolvedValue({ size: 1000 } as fs.Stats); - vi.spyOn(fs.promises, "rm").mockResolvedValue(undefined); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("should pass projects array to uploadSourceMaps when multiple projects configured", async () => { - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: ["proj-a", "proj-b", "proj-c"], - release: { name: "test-release" }, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - await buildPluginManager.uploadSourcemaps(["/path/to/bundle.js"]); - - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - "test-release", - expect.objectContaining({ - projects: ["proj-a", "proj-b", "proj-c"], - }) - ); - }); - - it("should pass single project as array to uploadSourceMaps", async () => { - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "single-project", - release: { name: "test-release" }, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - await buildPluginManager.uploadSourcemaps(["/path/to/bundle.js"]); - - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - "test-release", - expect.objectContaining({ - projects: ["single-project"], - }) - ); - }); - - it("should pass projects array in direct upload mode", async () => { - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: ["proj-a", "proj-b"], - release: { name: "test-release" }, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - await buildPluginManager.uploadSourcemaps(["/path/to/bundle.js"], { - prepareArtifacts: false, - }); - - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - "test-release", - expect.objectContaining({ - projects: ["proj-a", "proj-b"], - }) - ); - }); - }); - - describe("moduleMetadata callback with multiple projects", () => { - it("should pass project as string and projects as array when multiple projects configured", () => { - const moduleMetadataCallback = vi.fn().mockReturnValue({ custom: "metadata" }); - - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: ["proj-a", "proj-b", "proj-c"], - release: { name: "test-release" }, - moduleMetadata: moduleMetadataCallback, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(moduleMetadataCallback).toHaveBeenCalledWith({ - org: "test-org", - project: "proj-a", - projects: ["proj-a", "proj-b", "proj-c"], - release: "test-release", - }); - }); - - it("should pass project as string and projects as array with single project", () => { - const moduleMetadataCallback = vi.fn().mockReturnValue({ custom: "metadata" }); - - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "single-project", - release: { name: "test-release" }, - moduleMetadata: moduleMetadataCallback, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(moduleMetadataCallback).toHaveBeenCalledWith({ - org: "test-org", - project: "single-project", - projects: ["single-project"], - release: "test-release", - }); - }); - - it("should pass undefined for projects when no project configured", () => { - const moduleMetadataCallback = vi.fn().mockReturnValue({ custom: "metadata" }); - - createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - release: { name: "test-release" }, - moduleMetadata: moduleMetadataCallback, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - expect(moduleMetadataCallback).toHaveBeenCalledWith({ - org: "test-org", - project: undefined, - projects: undefined, - release: "test-release", - }); - }); - }); - - describe("telemetry option", () => { - it("should not pass sentry-trace or baggage headers to CLI when telemetry is false", async () => { - mockCliExecute.mockResolvedValue(undefined); - - const buildPluginManager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - telemetry: false, - }, - { - buildTool: "webpack", - loggerPrefix: "[sentry-webpack-plugin]", - } - ); - - // Trigger a CLI operation so createCliInstance is called - await buildPluginManager.injectDebugIds(["/path/to/bundle"]); - - // Find the CLI constructor call that was made by createCliInstance (not the one from allowedToSendTelemetry) - const cliConstructorCalls = mockCliConstructor.mock.calls; - expect(cliConstructorCalls.length).toBeGreaterThan(0); - - // Check that none of the CLI instances were created with sentry-trace or baggage headers - for (const call of cliConstructorCalls) { - const options = call[1] as { headers?: Record }; - if (options?.headers) { - expect(options.headers).not.toHaveProperty("sentry-trace"); - expect(options.headers).not.toHaveProperty("baggage"); - } - } - }); - }); - - describe("createRelease deploy deduplication", () => { - beforeEach(() => { - vi.clearAllMocks(); - _resetDeployedReleasesForTesting(); - }); - - it("should create a deploy record on the first call", async () => { - const manager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { - name: "test-release", - deploy: { env: "production" }, - }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.createRelease(); - - expect(mockCliNewDeploy).toHaveBeenCalledTimes(1); - expect(mockCliNewDeploy).toHaveBeenCalledWith("test-release", { env: "production" }); - }); - - it("should not create duplicate deploy records when createRelease is called multiple times on the same instance", async () => { - const manager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { - name: "test-release", - deploy: { env: "production" }, - }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.createRelease(); - await manager.createRelease(); - await manager.createRelease(); - - expect(mockCliNewDeploy).toHaveBeenCalledTimes(1); - }); - - it("should not create duplicate deploy records across separate plugin instances with the same release name", async () => { - const managerA = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { - name: "test-release", - deploy: { env: "production" }, - }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - const managerB = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { - name: "test-release", - deploy: { env: "production" }, - }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await managerA.createRelease(); - await managerB.createRelease(); - - expect(mockCliNewDeploy).toHaveBeenCalledTimes(1); - }); - - it("should allow deploys for different release names", async () => { - const managerA = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { - name: "release-1", - deploy: { env: "production" }, - }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - const managerB = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { - name: "release-2", - deploy: { env: "production" }, - }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await managerA.createRelease(); - await managerB.createRelease(); - - expect(mockCliNewDeploy).toHaveBeenCalledTimes(2); - expect(mockCliNewDeploy).toHaveBeenCalledWith("release-1", { env: "production" }); - expect(mockCliNewDeploy).toHaveBeenCalledWith("release-2", { env: "production" }); - }); - - it("should not create a deploy when deploy option is not set", async () => { - const manager = createSentryBuildPluginManager( - { - authToken: "test-token", - org: "test-org", - project: "test-project", - release: { name: "test-release" }, - }, - { buildTool: "webpack", loggerPrefix: "[sentry-webpack-plugin]" } - ); - - await manager.createRelease(); - - expect(mockCliNewDeploy).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/bundler-plugins/test/core/debug-id-upload.test.ts b/packages/bundler-plugins/test/core/debug-id-upload.test.ts deleted file mode 100644 index f4651fea..00000000 --- a/packages/bundler-plugins/test/core/debug-id-upload.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import { prepareBundleForDebugIdUpload } from "../../src/core/debug-id-upload"; -import type { RewriteSourcesHook } from "../../src/core/types"; -import type { Logger } from "../../src/core"; - -describe("prepareBundleForDebugIdUpload", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sentry-test-")); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it("passes mapDir context to rewriteSources hook", async () => { - const bundleDir = path.join(tmpDir, "src"); - const uploadDir = path.join(tmpDir, "upload"); - fs.mkdirSync(bundleDir, { recursive: true }); - fs.mkdirSync(uploadDir, { recursive: true }); - - const debugId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; - const bundlePath = path.join(bundleDir, "bundle.js"); - const mapPath = path.join(bundleDir, "bundle.js.map"); - - // Bundle with debug ID snippet and sourceMappingURL - fs.writeFileSync( - bundlePath, - `"use strict";\n// some code\n;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();\n//# sourceMappingURL=bundle.js.map` - ); - - // Source map file - fs.writeFileSync( - mapPath, - JSON.stringify({ - version: 3, - sources: ["../original/file.ts"], - mappings: "AAAA", - }) - ); - - const capturedContexts: Array<{ mapDir?: string } | undefined> = []; - const rewriteHook: RewriteSourcesHook = (source, _map, context) => { - capturedContexts.push(context); - return source; - }; - - const logger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }; - - await prepareBundleForDebugIdUpload( - bundlePath, - uploadDir, - 0, - logger as Logger, - rewriteHook, - undefined - ); - - expect(capturedContexts).toHaveLength(1); - expect(capturedContexts[0]!.mapDir).toBe(bundleDir); - }); -}); diff --git a/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/index.js deleted file mode 100644 index 756d8189..00000000 --- a/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/index.js +++ /dev/null @@ -1 +0,0 @@ -// Placeholder here diff --git a/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/package.json deleted file mode 100644 index f1ae4a21..00000000 --- a/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "invalid": "not a real package.json" -} diff --git a/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/package.json b/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/package.json deleted file mode 100644 index c656da6e..00000000 --- a/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "my-deeply-nested-package" -} diff --git a/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/index.js deleted file mode 100644 index 6bc5ef9c..00000000 --- a/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/index.js +++ /dev/null @@ -1 +0,0 @@ -// Placeholder here \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/package.json deleted file mode 100644 index b9ea9c4f..00000000 --- a/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/package.json +++ /dev/null @@ -1 +0,0 @@ -// This is an invalid json File!! \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/nested-error-package/package.json b/packages/bundler-plugins/test/core/fixtures/nested-error-package/package.json deleted file mode 100644 index 9bc6b6d5..00000000 --- a/packages/bundler-plugins/test/core/fixtures/nested-error-package/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "my-deeply-nested-package" -} \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/index.js deleted file mode 100644 index 6bc5ef9c..00000000 --- a/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/index.js +++ /dev/null @@ -1 +0,0 @@ -// Placeholder here \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/package.json deleted file mode 100644 index 2dec0988..00000000 --- a/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "my-first-package" -} \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/nested-package/package.json b/packages/bundler-plugins/test/core/fixtures/nested-package/package.json deleted file mode 100644 index 9bc6b6d5..00000000 --- a/packages/bundler-plugins/test/core/fixtures/nested-package/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "my-deeply-nested-package" -} \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/index.js deleted file mode 100644 index 6bc5ef9c..00000000 --- a/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/index.js +++ /dev/null @@ -1 +0,0 @@ -// Placeholder here \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/package.json deleted file mode 100644 index a04620ef..00000000 --- a/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "invalid": "my-first-package" -} \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js deleted file mode 100644 index ff1a49c7..00000000 --- a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -console.log("wow!"); \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map deleted file mode 100644 index 11ab57d0..00000000 --- a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"input.js","sourceRoot":"","sources":["input.tsx"],"names":[],"mappings":";AAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA"} \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/bundles/index.js b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/bundles/index.js deleted file mode 100644 index ff1a49c7..00000000 --- a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/bundles/index.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -console.log("wow!"); \ No newline at end of file diff --git a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map deleted file mode 100644 index 88e40095..00000000 --- a/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"input.js","sourceRoot":"","sources":["input.tsx"],"names":[],"mappings":";AAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA"} diff --git a/packages/bundler-plugins/test/core/glob.test.ts b/packages/bundler-plugins/test/core/glob.test.ts deleted file mode 100644 index 9411a7bc..00000000 --- a/packages/bundler-plugins/test/core/glob.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import { globFiles } from "../../src/core/glob"; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; - -let tmpDir: string; - -beforeEach(async () => { - tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "glob-test-")); -}); - -afterEach(async () => { - await fs.promises.rm(tmpDir, { recursive: true, force: true }); -}); - -/** Helper: create a file (and any parent dirs) under tmpDir. */ -async function touch(...segments: string[]): Promise { - const filePath = path.join(tmpDir, ...segments); - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); - await fs.promises.writeFile(filePath, ""); - return filePath; -} - -describe("globFiles", () => { - describe("core behavior", () => { - it("returns absolute paths", async () => { - await touch("a.js"); - const result = await globFiles(path.join(tmpDir, "**/*.js")); - expect(result).toHaveLength(1); - expect(path.isAbsolute(result[0]!)).toBe(true); - }); - - it("excludes directories (nodir)", async () => { - // Create a directory that matches the glob pattern - await fs.promises.mkdir(path.join(tmpDir, "subdir.js"), { recursive: true }); - await touch("real.js"); - const result = await globFiles(path.join(tmpDir, "**/*.js")); - expect(result).toEqual([path.join(tmpDir, "real.js")]); - }); - - it("returns [] for no matches", async () => { - await touch("a.txt"); - const result = await globFiles(path.join(tmpDir, "**/*.js")); - expect(result).toEqual([]); - }); - - it("returns [] for nonexistent pattern path", async () => { - const result = await globFiles(path.join(tmpDir, "nonexistent/**/*.js")); - expect(result).toEqual([]); - }); - - it("matches deeply nested files", async () => { - const filePath = await touch("a", "b", "c", "deep.js"); - const result = await globFiles(path.join(tmpDir, "**/*.js")); - expect(result).toEqual([filePath]); - }); - - it("works with a string pattern", async () => { - await touch("single.js"); - const result = await globFiles(path.join(tmpDir, "*.js")); - expect(result).toHaveLength(1); - }); - - it("works with an array of patterns", async () => { - const jsFile = await touch("a.js"); - const mapFile = await touch("a.js.map"); - await touch("b.css"); - - const result = await globFiles([ - path.join(tmpDir, "**/*.js"), - path.join(tmpDir, "**/*.js.map"), - ]); - result.sort(); - expect(result).toEqual([jsFile, mapFile].sort()); - }); - }); - - describe("root option", () => { - it("scopes results to root directory", async () => { - await touch("file.js"); - // Patterns starting with / are resolved relative to root - const result = await globFiles("/**/*.js", { root: tmpDir }); - expect(result).toEqual([path.join(tmpDir, "file.js")]); - }); - }); - - describe("ignore option", () => { - it("excludes files matching ignore string pattern", async () => { - await touch("keep.js"); - await touch("node_modules", "dep.js"); - - const result = await globFiles(path.join(tmpDir, "**/*.js"), { - ignore: path.join(tmpDir, "node_modules/**"), - }); - expect(result).toEqual([path.join(tmpDir, "keep.js")]); - }); - - it("excludes files matching ignore array patterns", async () => { - await touch("keep.js"); - await touch("node_modules", "dep.js"); - await touch("dist", "bundle.js"); - - const result = await globFiles(path.join(tmpDir, "**/*.js"), { - ignore: [path.join(tmpDir, "node_modules/**"), path.join(tmpDir, "dist/**")], - }); - expect(result).toEqual([path.join(tmpDir, "keep.js")]); - }); - }); - - describe("rollup JS/map patterns", () => { - const JS_AND_MAP_PATTERNS = [ - "/**/*.js", - "/**/*.mjs", - "/**/*.cjs", - "/**/*.js.map", - "/**/*.mjs.map", - "/**/*.cjs.map", - ].map((q) => `${q}?(\\?*)?(#*)`); - - it("matches .js, .mjs, .cjs and their .map variants", async () => { - const files = await Promise.all([ - touch("a.js"), - touch("b.mjs"), - touch("c.cjs"), - touch("a.js.map"), - touch("b.mjs.map"), - touch("c.cjs.map"), - ]); - - const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir }); - result.sort(); - expect(result).toEqual(files.sort()); - }); - - it("does NOT match .css, .ts, .json, etc.", async () => { - await touch("style.css"); - await touch("types.ts"); - await touch("data.json"); - await touch("readme.md"); - - const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir }); - expect(result).toEqual([]); - }); - - it("works in nested subdirectories", async () => { - const files = await Promise.all([ - touch("src", "deep", "a.js"), - touch("src", "deep", "a.js.map"), - ]); - - const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir }); - result.sort(); - expect(result).toEqual(files.sort()); - }); - - it("returns [] for empty directory", async () => { - const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir }); - expect(result).toEqual([]); - }); - }); -}); diff --git a/packages/bundler-plugins/test/core/index.test.ts b/packages/bundler-plugins/test/core/index.test.ts deleted file mode 100644 index 5f9ee192..00000000 --- a/packages/bundler-plugins/test/core/index.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { getDebugIdSnippet } from "../../src/core"; -import { containsOnlyImports } from "../../src/core/utils"; -import { describe, it, expect } from "vitest"; - -describe("getDebugIdSnippet", () => { - it("returns the debugId injection snippet for a passed debugId", () => { - const snippet = getDebugIdSnippet("1234"); - expect(snippet.code()).toMatchInlineSnapshot( - `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1234",e._sentryDebugIdIdentifier="sentry-dbid-1234");}catch(e){}}();"` - ); - }); -}); - -describe("containsOnlyImports", () => { - describe("should return true (import-only code)", () => { - it.each([ - ["empty string", ""], - ["whitespace only", " \n\t "], - ["side effect import with single quotes", "import './module.js';"], - ["side effect import with double quotes", 'import "./module.js";'], - ["side effect import with backticks", "import `./module.js`;"], - ["side effect import without semicolon", "import './module.js'"], - ["default import", "import foo from './module.js';"], - ["named import", "import { foo } from './module.js';"], - ["named import with alias", "import { foo as bar } from './module.js';"], - ["multiple named imports", "import { foo, bar, baz } from './module.js';"], - ["namespace import", "import * as utils from './utils.js';"], - ["default and named imports", "import React, { useState } from 'react';"], - ["re-export all", "export * from './module.js';"], - ["re-export named", "export { foo, bar } from './module.js';"], - ["re-export with alias", "export { foo as default } from './module.js';"], - ])("%s", (_, code) => { - expect(containsOnlyImports(code)).toBe(true); - }); - - it.each([ - [ - "multiple imports", - ` -import './polyfill.js'; -import { helper } from './utils.js'; -import config from './config.js'; -`, - ], - [ - "imports with line comments", - ` -// This is a comment -import './module.js'; -// Another comment -`, - ], - [ - "imports with block comments", - ` -/* Block comment */ -import './module.js'; -/* Multi - line - comment */ -`, - ], - ["'use strict' with imports", `"use strict";\nimport './module.js';`], - ["'use strict' with single quotes", `'use strict';\nimport './module.js';`], - [ - "mixed imports, re-exports, and comments", - ` -"use strict"; -// Entry point facade -import './polyfills.js'; -import { init } from './app.js'; -/* Re-export for external use */ -export * from './types.js'; -export { config } from './config.js'; -`, - ], - ])("%s", (_, code) => { - expect(containsOnlyImports(code)).toBe(true); - }); - }); - - describe("should return false (contains substantial code)", () => { - it.each([ - ["variable declaration", "const x = 1;"], - ["let declaration", "let y = 2;"], - ["var declaration", "var z = 3;"], - ["function declaration", "function foo() {}"], - ["arrow function", "const fn = () => {};"], - ["class declaration", "class MyClass {}"], - ["function call", "console.log('hello');"], - ["IIFE", "(function() {})();"], - ["expression statement", "1 + 1;"], - ["object literal", "({ foo: 'bar' });"], - ["export declaration (not re-export)", "export const foo = 1;"], - ["export default expression", "export default {};"], - ["export function", "export function foo() {}"], - ["minified bundle code", `import{a as e}from"./chunk.js";var t=function(){return e()};t();`], - ])("%s", (_, code) => { - expect(containsOnlyImports(code)).toBe(false); - }); - - // Multi-line code snippets - it.each([ - [ - "import followed by code", - ` -import { init } from './app.js'; -init(); -`, - ], - [ - "import with variable declaration", - ` -import './module.js'; -const config = { debug: true }; -`, - ], - [ - "import with function declaration", - ` -import { helper } from './utils.js'; -function main() { - helper(); -} -`, - ], - [ - "real-world SPA bundle snippet", - ` -import { createApp } from 'vue'; -import App from './App.vue'; -import router from './router'; - -const app = createApp(App); -app.use(router); -app.mount('#app'); -`, - ], - ])("%s", (_, code) => { - expect(containsOnlyImports(code)).toBe(false); - }); - }); -}); diff --git a/packages/bundler-plugins/test/core/option-mappings.test.ts b/packages/bundler-plugins/test/core/option-mappings.test.ts deleted file mode 100644 index c50d5187..00000000 --- a/packages/bundler-plugins/test/core/option-mappings.test.ts +++ /dev/null @@ -1,372 +0,0 @@ -import type { Options } from "../../src/core"; -import type { NormalizedOptions } from "../../src/core/options-mapping"; -import { normalizeUserOptions, validateOptions } from "../../src/core/options-mapping"; -import { describe, it, test, expect, afterEach, vi, beforeEach } from "vitest"; - -describe("normalizeUserOptions()", () => { - test("should return correct value for default input", () => { - const userOptions: Options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - release: { name: "my-release", uploadLegacySourcemaps: "./out" }, // we have to define this even though it is an optional value because of auto discovery - }; - - expect(normalizeUserOptions(userOptions)).toEqual({ - authToken: "my-auth-token", - org: "my-org", - project: "my-project", - debug: false, - disable: false, - release: { - name: "my-release", - finalize: true, - inject: true, - create: true, - vcsRemote: "origin", - uploadLegacySourcemaps: "./out", - setCommits: { - auto: true, - shouldNotThrowOnFailure: true, - ignoreEmpty: true, - ignoreMissing: true, - }, - }, - silent: false, - telemetry: true, - _experiments: {}, - _metaOptions: { - telemetry: { - metaFramework: undefined, - }, - }, - url: "https://sentry.io", - }); - }); - - test("should hoist top-level include options into include entries", () => { - const userOptions: Options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - release: { - name: "my-release", // we have to define this even though it is an optional value because of auto discovery - uploadLegacySourcemaps: { - paths: ["./output", "./files"], - ignore: ["./files"], - rewrite: true, - sourceMapReference: false, - stripCommonPrefix: true, - ext: ["js", "map", ".foo"], - }, - }, - }; - - expect(normalizeUserOptions(userOptions)).toEqual({ - authToken: "my-auth-token", - org: "my-org", - project: "my-project", - debug: false, - disable: false, - release: { - name: "my-release", - vcsRemote: "origin", - finalize: true, - create: true, - inject: true, - uploadLegacySourcemaps: { - ext: ["js", "map", ".foo"], - ignore: ["./files"], - paths: ["./output", "./files"], - rewrite: true, - sourceMapReference: false, - stripCommonPrefix: true, - }, - setCommits: { - auto: true, - shouldNotThrowOnFailure: true, - ignoreEmpty: true, - ignoreMissing: true, - }, - }, - silent: false, - telemetry: true, - _experiments: {}, - _metaOptions: { - telemetry: { - metaFramework: undefined, - }, - }, - url: "https://sentry.io", - }); - }); - - test.each(["https://sentry.io", undefined])( - "should enable telemetry if `telemetry` is true and Sentry SaaS URL (%s) is used", - (url) => { - const options = { - include: "", - url, - }; - - expect(normalizeUserOptions(options).telemetry).toBe(true); - } - ); - - describe("Vercel deploy detection", () => { - const originalEnv = process.env; - - beforeEach(() => { - process.env = { ...originalEnv }; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - test("should automatically create deploy config when Vercel env vars are present", () => { - process.env["VERCEL"] = "1"; - process.env["VERCEL_TARGET_ENV"] = "production"; - process.env["VERCEL_URL"] = "my-app.vercel.app"; - - const userOptions: Options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - release: { name: "my-release" }, - }; - - const normalizedOptions = normalizeUserOptions(userOptions); - - expect(normalizedOptions.release.deploy).toEqual({ - env: "vercel-production", - url: "https://my-app.vercel.app", - }); - }); - - test("should not create deploy config when deploy is explicitly set to false", () => { - process.env["VERCEL"] = "1"; - process.env["VERCEL_TARGET_ENV"] = "production"; - process.env["VERCEL_URL"] = "my-app.vercel.app"; - - const userOptions: Options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - release: { name: "my-release", deploy: false }, - }; - - const normalizedOptions = normalizeUserOptions(userOptions); - - expect(normalizedOptions.release.deploy).toBe(false); - }); - - test("should not override manually provided deploy config", () => { - process.env["VERCEL"] = "1"; - process.env["VERCEL_TARGET_ENV"] = "production"; - process.env["VERCEL_URL"] = "my-app.vercel.app"; - - const manualDeployConfig = { env: "custom-env", name: "custom-deploy" }; - const userOptions: Options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - release: { name: "my-release", deploy: manualDeployConfig }, - }; - - const normalizedOptions = normalizeUserOptions(userOptions); - - expect(normalizedOptions.release.deploy).toEqual(manualDeployConfig); - }); - - test("should not create deploy config when Vercel env vars are missing", () => { - const userOptions: Options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - release: { name: "my-release" }, - }; - - const normalizedOptions = normalizeUserOptions(userOptions); - - expect(normalizedOptions.release.deploy).toBeUndefined(); - }); - }); - - describe("multi-project support", () => { - test("should accept project as a string array", () => { - const userOptions: Options = { - org: "my-org", - project: ["project-a", "project-b", "project-c"], - authToken: "my-auth-token", - release: { name: "my-release" }, - }; - - const normalized = normalizeUserOptions(userOptions); - expect(normalized.project).toEqual(["project-a", "project-b", "project-c"]); - }); - - test("should parse comma-separated SENTRY_PROJECT env var", () => { - const originalEnv = process.env; - process.env = { ...originalEnv }; - process.env["SENTRY_PROJECT"] = "proj1,proj2,proj3"; - - const userOptions: Options = { - org: "my-org", - authToken: "my-auth-token", - }; - - const normalized = normalizeUserOptions(userOptions); - expect(normalized.project).toEqual(["proj1", "proj2", "proj3"]); - - process.env = originalEnv; - }); - - test("should trim whitespace from comma-separated projects", () => { - const originalEnv = process.env; - process.env = { ...originalEnv }; - process.env["SENTRY_PROJECT"] = "proj1 , proj2 , proj3"; - - const userOptions: Options = { - org: "my-org", - authToken: "my-auth-token", - }; - - const normalized = normalizeUserOptions(userOptions); - expect(normalized.project).toEqual(["proj1", "proj2", "proj3"]); - - process.env = originalEnv; - }); - - test("should keep single project as string (no comma)", () => { - const originalEnv = process.env; - process.env = { ...originalEnv }; - process.env["SENTRY_PROJECT"] = "single-project"; - - const userOptions: Options = { - org: "my-org", - authToken: "my-auth-token", - }; - - const normalized = normalizeUserOptions(userOptions); - expect(normalized.project).toBe("single-project"); - - process.env = originalEnv; - }); - }); -}); - -describe("validateOptions", () => { - const mockedLogger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; - - afterEach(() => { - vi.resetAllMocks(); - }); - - it("should return `true` if `injectRelease` is `true` and org is provided", () => { - const options = { injectReleasesMap: true, org: "my-org" } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - }); - - it("should return `false` if `setCommits` is set but neither auto nor manual options are set", () => { - const options = { release: { setCommits: {} } } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith( - expect.stringMatching(/setCommits.*missing.*properties/), - expect.stringMatching(/set.*either.*auto.*repo.*commit/) - ); - }); - - it("should return `true` but warn if `setCommits` is set and both auto nor manual options are set", () => { - const options = { release: { setCommits: { auto: true, repo: "myRepo", commit: "myCommit" } } }; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - expect(mockedLogger.warn).toHaveBeenCalledWith( - expect.stringMatching(/setCommits.*auto.*repo.*commit/), - expect.stringMatching(/Ignoring.*repo.*commit/), - expect.stringMatching(/set.*either.*auto.*repo.*commit/) - ); - }); - - it("should return `false` if `deploy`is set but `env` is not provided", () => { - const options = { release: { deploy: {} } } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith( - expect.stringMatching(/deploy.*missing.*property/), - expect.stringMatching(/set.*env/) - ); - }); - - it("should return `true` if `deploy`is set and `env` is provided", () => { - const options = { release: { deploy: { env: "my-env" } } } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - }); - - it("should return `true` if `deploy` is set to `false`", () => { - const options = { release: { deploy: false } } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - }); - - it("should return `true` for options without special cases", () => { - const options = { - org: "my-org", - project: "my-project", - authToken: "my-auth-token", - include: [{}], - finalize: true, - } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - }); - - describe("multi-project validation", () => { - it("should return `false` if project array is empty", () => { - const options = { project: [] } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith( - expect.stringMatching(/project.*array.*empty/i), - expect.stringMatching(/at least one/i) - ); - }); - - it("should return `false` if project array contains invalid strings", () => { - const options = { project: ["valid", "", " ", "also-valid"] } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith( - expect.stringMatching(/invalid.*project/i), - expect.stringMatching(/non-empty strings/i) - ); - }); - - it("should return `true` for valid project array", () => { - const options = { project: ["proj-a", "proj-b"] } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - }); - - it("should return `true` for valid single project string", () => { - const options = { project: "single-project" } as Partial; - - expect(validateOptions(options as unknown as NormalizedOptions, mockedLogger)).toBe(true); - expect(mockedLogger.error).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/bundler-plugins/test/core/sentry/logger.test.ts b/packages/bundler-plugins/test/core/sentry/logger.test.ts deleted file mode 100644 index 0e265569..00000000 --- a/packages/bundler-plugins/test/core/sentry/logger.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createLogger } from "../../../src/core/logger"; -import { describe, it, expect, vi, afterEach } from "vitest"; - -describe("Logger", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - const consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => undefined); - const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); - const consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => undefined); - - afterEach(() => { - consoleErrorSpy.mockReset(); - consoleInfoSpy.mockReset(); - consoleWarnSpy.mockReset(); - consoleDebugSpy.mockReset(); - }); - - it.each([ - ["info", "Info", consoleInfoSpy], - ["warn", "Warning", consoleWarnSpy], - ["error", "Error", consoleErrorSpy], - ] as const)(".%s() should log correctly", (loggerMethod, logLevel, consoleSpy) => { - const prefix = "[some-prefix]"; - const logger = createLogger({ prefix, silent: false, debug: true }); - - logger[loggerMethod]("Hey!"); - - expect(consoleSpy).toHaveBeenCalledWith(`[some-prefix] ${logLevel}: Hey!`); - }); - - it.each([ - ["info", "Info", consoleInfoSpy], - ["warn", "Warning", consoleWarnSpy], - ["error", "Error", consoleErrorSpy], - ] as const)( - ".%s() should log multiple params correctly", - (loggerMethod, logLevel, consoleSpy) => { - const prefix = "[some-prefix]"; - const logger = createLogger({ prefix, silent: false, debug: true }); - - logger[loggerMethod]("Hey!", "this", "is", "a test with", 5, "params"); - - expect(consoleSpy).toHaveBeenCalledWith( - `[some-prefix] ${logLevel}: Hey!`, - "this", - "is", - "a test with", - 5, - "params" - ); - } - ); - - it(".debug() should log correctly", () => { - const prefix = "[some-prefix]"; - const logger = createLogger({ prefix, silent: false, debug: true }); - - logger.debug("Hey!"); - - expect(consoleDebugSpy).toHaveBeenCalledWith(`[some-prefix] Debug: Hey!`); - }); - - it(".debug() should log multiple params correctly", () => { - const prefix = "[some-prefix]"; - const logger = createLogger({ prefix, silent: false, debug: true }); - - logger.debug("Hey!", "this", "is", "a test with", 5, "params"); - - expect(consoleDebugSpy).toHaveBeenCalledWith( - `[some-prefix] Debug: Hey!`, - "this", - "is", - "a test with", - 5, - "params" - ); - }); - - describe("doesn't log when `silent` option is `true`", () => { - it.each([ - ["info", consoleInfoSpy], - ["warn", consoleWarnSpy], - ["error", consoleErrorSpy], - ] as const)(".%s()", (loggerMethod, consoleSpy) => { - const prefix = "[some-prefix]"; - const logger = createLogger({ prefix, silent: true, debug: true }); - - logger[loggerMethod]("Hey!"); - - expect(consoleSpy).not.toHaveBeenCalled(); - }); - }); - - it(".debug() doesn't log when `silent` option is `true`", () => { - const prefix = "[some-prefix]"; - const logger = createLogger({ prefix, silent: true, debug: true }); - - logger.debug("Hey!"); - - expect(consoleDebugSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/bundler-plugins/test/core/sentry/resolve-source-maps.test.ts b/packages/bundler-plugins/test/core/sentry/resolve-source-maps.test.ts deleted file mode 100644 index b7a5a2af..00000000 --- a/packages/bundler-plugins/test/core/sentry/resolve-source-maps.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import * as path from "path"; -import * as fs from "fs"; -import * as url from "url"; -import { determineSourceMapPathFromBundle } from "../../../src/core/debug-id-upload"; -import { createLogger } from "../../../src/core/logger"; -import { describe, it, expect, vi } from "vitest"; - -const logger = createLogger({ prefix: "[resolve-source-maps-test]", silent: false, debug: false }); -const fixtureDir = path.resolve(__dirname, "../fixtures/resolve-source-maps"); - -const adjacentBundlePath = path.join(fixtureDir, "adjacent-sourcemap/index.js"); -const adjacentSourceMapPath = path.join(fixtureDir, "adjacent-sourcemap/index.js.map"); -const adjacentBundleContent = fs.readFileSync(adjacentBundlePath, "utf-8"); - -const separateBundlePath = path.join(fixtureDir, "separate-directory/bundles/index.js"); -const separateSourceMapPath = path.join(fixtureDir, "separate-directory/sourcemaps/index.js.map"); -const separateBundleContent = fs.readFileSync(separateBundlePath, "utf-8"); - -const sourceMapUrl = "https://sourcemaps.example.com/foo/index.js.map"; - -function srcMappingUrl(url: string): string { - return `\n//# sourceMappingURL=${url}`; -} - -describe("Resolve source maps", () => { - it("should resolve source maps next to bundles", async () => { - expect( - await determineSourceMapPathFromBundle( - adjacentBundlePath, - adjacentBundleContent, - logger, - undefined - ) - ).toEqual(adjacentSourceMapPath); - }); - - it("shouldn't resolve source maps in separate directories", async () => { - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent, - logger, - undefined - ) - ).toBeUndefined(); - }); - - describe("sourceMappingURL resolution", () => { - it("should resolve source maps when sourceMappingURL is a file URL", async () => { - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent + srcMappingUrl(url.pathToFileURL(separateSourceMapPath).href), - logger, - undefined - ) - ).toEqual(separateSourceMapPath); - }); - - it("shouldn't resolve source maps when sourceMappingURL is a non-file URL", async () => { - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent + srcMappingUrl(sourceMapUrl), - logger, - undefined - ) - ).toBeUndefined(); - }); - - it("should resolve source maps when sourceMappingURL is an absolute path", async () => { - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent + srcMappingUrl(separateSourceMapPath), - logger, - undefined - ) - ).toEqual(separateSourceMapPath); - }); - - it("should resolve source maps when sourceMappingURL is a relative path", async () => { - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent + - srcMappingUrl(path.relative(path.dirname(separateBundlePath), separateSourceMapPath)), - logger, - undefined - ) - ).toEqual(separateSourceMapPath); - }); - }); - - describe("resolveSourceMap hook", () => { - it("should resolve source maps when a resolveSourceMap hook is provided", async () => { - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent + srcMappingUrl(sourceMapUrl), - logger, - () => separateSourceMapPath - ) - ).toEqual(separateSourceMapPath); - }); - - it("should pass the correct values to the resolveSourceMap hook", async () => { - const hook = vi.fn(() => separateSourceMapPath); - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent + srcMappingUrl(sourceMapUrl), - logger, - hook - ) - ).toEqual(separateSourceMapPath); - expect(hook.mock.calls[0]).toEqual([separateBundlePath, sourceMapUrl]); - }); - - it("should pass the correct values to the resolveSourceMap hook when no sourceMappingURL is present", async () => { - const hook = vi.fn(() => separateSourceMapPath); - expect( - await determineSourceMapPathFromBundle( - separateBundlePath, - separateBundleContent, - logger, - hook - ) - ).toEqual(separateSourceMapPath); - expect(hook.mock.calls[0]).toEqual([separateBundlePath, undefined]); - }); - - it("should prefer resolveSourceMap result over heuristic results", async () => { - expect( - await determineSourceMapPathFromBundle( - adjacentBundlePath, - adjacentBundleContent, - logger, - () => separateSourceMapPath - ) - ).toEqual(separateSourceMapPath); - }); - - it("should fall back when the resolveSourceMap hook returns undefined", async () => { - expect( - await determineSourceMapPathFromBundle( - adjacentBundlePath, - adjacentBundleContent, - logger, - () => undefined - ) - ).toEqual(adjacentSourceMapPath); - }); - - it("should fall back when the resolveSourceMap hook returns a non-existent path", async () => { - expect( - await determineSourceMapPathFromBundle( - adjacentBundlePath, - adjacentBundleContent, - logger, - () => path.join(fixtureDir, "non-existent.js.map") - ) - ).toEqual(adjacentSourceMapPath); - }); - }); -}); diff --git a/packages/bundler-plugins/test/core/sentry/telemetry.test.ts b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts deleted file mode 100644 index 5474f6e5..00000000 --- a/packages/bundler-plugins/test/core/sentry/telemetry.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { Scope } from "@sentry/core"; -import type { NormalizedOptions } from "../../../src/core/options-mapping"; -import { normalizeUserOptions } from "../../../src/core/options-mapping"; -import { - allowedToSendTelemetry, - setTelemetryDataOnScope, -} from "../../../src/core/sentry/telemetry"; -import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; - -const { mockCliExecute } = vi.hoisted(() => ({ - mockCliExecute: vi.fn(), -})); - -vi.mock("@sentry/cli", () => ({ - default: class { - execute = mockCliExecute; - }, -})); - -describe("shouldSendTelemetry", () => { - afterEach(() => { - vi.resetAllMocks(); - }); - - it("should return false if CLI returns a URL other than sentry.io", async () => { - mockCliExecute.mockImplementation( - () => "Sentry Server: https://selfhostedSentry.io \nsomeotherstuff\netc" - ); - expect(await allowedToSendTelemetry({ release: {} } as NormalizedOptions)).toBe(false); - }); - - it("should return true if CLI returns sentry.io as a URL", async () => { - mockCliExecute.mockImplementation( - () => "Sentry Server: https://sentry.io \nsomeotherstuff\netc" - ); - expect(await allowedToSendTelemetry({ release: {} } as NormalizedOptions)).toBe(true); - }); -}); - -describe("addPluginOptionTagsToScope", () => { - const mockedScope = { - setTag: vi.fn(), - setTags: vi.fn(), - setUser: vi.fn(), - }; - - const defaultOptions = { - release: { uploadLegacySourcemaps: [] }, - }; - - beforeEach(() => { - vi.resetAllMocks(); - }); - - it("should set include tag according to number of entries (single entry)", () => { - setTelemetryDataOnScope( - normalizeUserOptions(defaultOptions), - mockedScope as unknown as Scope, - "rollup" - ); - expect(mockedScope.setTag).toHaveBeenCalledWith("uploadLegacySourcemapsEntries", 0); - }); - - it("should set include tag according to number of entries (multiple entries)", () => { - setTelemetryDataOnScope( - normalizeUserOptions({ release: { uploadLegacySourcemaps: ["", "", ""] } }), - mockedScope as unknown as Scope, - "rollup" - ); - expect(mockedScope.setTag).toHaveBeenCalledWith("uploadLegacySourcemapsEntries", 3); - }); - - it("should set deploy tag to true if the deploy option is specified", () => { - setTelemetryDataOnScope( - normalizeUserOptions({ ...defaultOptions, release: { deploy: { env: "production" } } }), - mockedScope as unknown as Scope, - "rollup" - ); - expect(mockedScope.setTag).toHaveBeenCalledWith("deploy-options", true); - }); - - it("should set errorHandler tag to `custom` if the errorHandler option is specified", () => { - setTelemetryDataOnScope( - // eslint-disable-next-line @typescript-eslint/no-empty-function - normalizeUserOptions({ ...defaultOptions, errorHandler: () => {} }), - mockedScope as unknown as Scope, - "rollup" - ); - expect(mockedScope.setTag).toHaveBeenCalledWith("custom-error-handler", true); - }); - - it.each([ - ["auto", { auto: true }], - ["manual", { repo: "", commit: "" }], - ])( - `should set setCommits tag to %s if the setCommits option is %s`, - (expectedValue, commitOptions) => { - setTelemetryDataOnScope( - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any - normalizeUserOptions({ ...defaultOptions, release: { setCommits: commitOptions as any } }), - mockedScope as unknown as Scope, - "rollup" - ); - expect(mockedScope.setTag).toHaveBeenCalledWith("set-commits", expectedValue); - } - ); - - it("sets all simple tags correctly", () => { - setTelemetryDataOnScope( - normalizeUserOptions({ - ...defaultOptions, - release: { - finalize: true, - }, - }), - mockedScope as unknown as Scope, - "rollup" - ); - - expect(mockedScope.setTag).toHaveBeenCalledWith("finalize-release", true); - }); - - it("shouldn't set any tags other than include if no opional options are specified", () => { - setTelemetryDataOnScope( - normalizeUserOptions(defaultOptions), - mockedScope as unknown as Scope, - "rollup" - ); - - expect(mockedScope.setTag).toHaveBeenCalledWith("uploadLegacySourcemapsEntries", 0); - expect(mockedScope.setTag).toHaveBeenCalledWith("finalize-release", true); - expect(mockedScope.setTag).toHaveBeenCalledWith("node", expect.any(String)); - }); -}); diff --git a/packages/bundler-plugins/test/core/utils.test.ts b/packages/bundler-plugins/test/core/utils.test.ts deleted file mode 100644 index 1e07612d..00000000 --- a/packages/bundler-plugins/test/core/utils.test.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { - determineReleaseName, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, - getDependencies, - getPackageJson, - parseMajorVersion, - replaceBooleanFlagsInCode, - serializeIgnoreOptions, - stringToUUID, -} from "../../src/core/utils"; - -import childProcess from "child_process"; -import fs from "fs"; -import { describe, it, expect, test, vi } from "vitest"; -import path from "node:path"; - -type PackageJson = Record; - -function getCwdFor(dirName: string): string { - return path.resolve(__dirname + dirName); -} - -describe("getPackageJson", () => { - test("it works for this package", () => { - const packageJson = getPackageJson(); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const expected = require("../../package.json") as PackageJson; - - expect(packageJson).toEqual(expected); - }); - - test("it works with nested folders with invalid package.json format", () => { - const packageJson = getPackageJson({ - cwd: getCwdFor("/fixtures/deeply-nested-package/deeply/nested"), - }); - - expect(packageJson).toEqual({ name: "my-deeply-nested-package" }); - }); - - test("it works with nested folders with errors in package.json", () => { - const packageJson = getPackageJson({ - cwd: getCwdFor("/fixtures/nested-error-package/deeply/nested"), - }); - - expect(packageJson).toEqual({ name: "my-deeply-nested-package" }); - }); - - test("it picks first package.json it finds", () => { - const packageJson = getPackageJson({ - cwd: getCwdFor("/fixtures/nested-package/deeply/nested"), - }); - - expect(packageJson).toEqual({ name: "my-first-package" }); - }); - - test("it stops after reaching too far", () => { - const packageJson = getPackageJson({ - cwd: getCwdFor("/fixtures/no-valid-package/deeply/nested"), - stopAt: process.cwd(), - }); - - expect(packageJson).toBeUndefined(); - }); -}); - -describe("parseMajorVersion", () => { - it.each([ - ["2.0.0", 2], - ["12.0.0", 12], - ["12.0", 12], - ["12", 12], - [">12", 12], - ["<12", 11], - ["<=12", 12], - [">=12", 12], - [">12.0.0", 12], - ["<12.0.0", 11], - ["<=12.0.0", 12], - [">=12.0.0", 12], - [">= 12.0.0", 12], - ["<= 12.0.0", 12], - ["< 12.0.0", 11], - ["< 12.0.1", 12], - ["< 12.1", 12], - ["< 12.0", 11], - ["> 2", 2], - ["< 2", 1], - ["12.x", 12], - ["> 10 < 12", 10], - ])("parses %s", (version, expected) => { - expect(parseMajorVersion(version)).toBe(expected); - - // Also test with prerelease suffix - expect(parseMajorVersion(`${version}-alpha.1`)).toBe(expected); - - // Also test with v prefix - expect(parseMajorVersion(`v${version}`)).toBe(expected); - }); -}); - -describe("getDependencies", () => { - test("it works without dependencies", () => { - const { deps, depsVersions } = getDependencies({}); - - expect(deps).toEqual([]); - expect(depsVersions).toEqual({}); - }); - - test("it works with only dependencies", () => { - const { deps, depsVersions } = getDependencies({ - dependencies: { - dep1: "1", - "other-dep": "^2.0.0", - dep2: "~3.1.0", - }, - }); - - expect(deps).toEqual(["dep1", "dep2", "other-dep"]); - expect(depsVersions).toEqual({}); - }); - - test("it works with only devDependencies", () => { - const { deps, depsVersions } = getDependencies({ - devDependencies: { - dep1: "1", - "other-dep": "^2.0.0", - dep2: "~3.1.0", - }, - }); - - expect(deps).toEqual(["dep1", "dep2", "other-dep"]); - expect(depsVersions).toEqual({}); - }); - - test("it works with both devDependencies & dependencies", () => { - const { deps, depsVersions } = getDependencies({ - devDependencies: { - dep1: "1", - "other-dep": "^2.0.0", - dep2: "~3.1.0", - }, - dependencies: { - dep3: "2", - "another-dep": "^3.0.0", - }, - }); - - expect(deps).toEqual(["another-dep", "dep1", "dep2", "dep3", "other-dep"]); - expect(depsVersions).toEqual({}); - }); - - test("it extracts versions of packages we care about", () => { - const { deps, depsVersions } = getDependencies({ - devDependencies: { - dep1: "1", - webpack: "5.x", - react: "^18.2.0", - "other-dep": "^2.0.0", - dep2: "~3.1.0", - }, - dependencies: { - dep3: "2", - "another-dep": "^3.0.0", - vite: "^3.0.0", - }, - }); - - expect(deps).toEqual([ - "another-dep", - "dep1", - "dep2", - "dep3", - "other-dep", - "react", - "vite", - "webpack", - ]); - expect(depsVersions).toEqual({ - react: 18, - vite: 3, - webpack: 5, - }); - }); -}); - -describe("stringToUUID", () => { - test("should return a deterministic UUID", () => { - expect(stringToUUID("Nothing personnel kid")).toBe("95543648-7392-49e4-b46a-67dfd0235986"); - }); -}); - -describe("replaceBooleanFlagsInCode", () => { - test("it works without a match", () => { - const code = "const a = 1;"; - const result = replaceBooleanFlagsInCode(code, { __DEBUG_BUILD__: false }); - expect(result).toBeNull(); - }); - - test("it works with matches", () => { - const code = `const a = 1; -if (__DEBUG_BUILD__ && checkMe()) { - // do something -} -if (__DEBUG_BUILD__ && __RRWEB_EXCLUDE_CANVAS__) { - const a = __RRWEB_EXCLUDE_CANVAS__ ? 1 : 2; -}`; - const result = replaceBooleanFlagsInCode(code, { - __DEBUG_BUILD__: false, - __RRWEB_EXCLUDE_CANVAS__: true, - }); - expect(result).toEqual({ - code: `const a = 1; -if (false && checkMe()) { - // do something -} -if (false && true) { - const a = true ? 1 : 2; -}`, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - map: expect.anything(), - }); - }); -}); - -describe("generateReleaseInjectorCode", () => { - it("generates code with release", () => { - const generatedCode = generateReleaseInjectorCode({ - release: "1.2.3", - injectBuildInformation: false, - }); - - expect(generatedCode.code()).toMatchSnapshot(); - }); - - it("generates code with release and build information", () => { - vi.spyOn(fs, "readFileSync").mockReturnValueOnce( - JSON.stringify({ - name: "test-app", - dependencies: { - myDep: "^2.1.4", - }, - devDependencies: { - rollup: "^3.1.4", - }, - }) - ); - - const generatedCode = generateReleaseInjectorCode({ - release: "1.2.3", - injectBuildInformation: true, - }); - - expect(generatedCode.code()).toMatchSnapshot(); - }); -}); - -describe("generateModuleMetadataInjectorCode", () => { - it("generates code with empty metadata object", () => { - const generatedCode = generateModuleMetadataInjectorCode({}); - expect(generatedCode.code()).toMatchSnapshot(); - }); - - it("generates code with metadata object", () => { - const generatedCode = generateModuleMetadataInjectorCode({ - "file1.js": { - foo: "bar", - }, - "file2.js": { - bar: "baz", - }, - }); - expect(generatedCode.code()).toMatchSnapshot(); - }); -}); - -describe("serializeIgnoreOptions", () => { - it("returns default ignore options when undefined", () => { - const result = serializeIgnoreOptions(undefined); - expect(result).toEqual(["--ignore", "node_modules"]); - }); - - it("handles array of ignore patterns", () => { - const result = serializeIgnoreOptions(["dist", "**/build/**", "*.log"]); - expect(result).toEqual(["--ignore", "dist", "--ignore", "**/build/**", "--ignore", "*.log"]); - }); - - it("handles single string pattern", () => { - const result = serializeIgnoreOptions("dist"); - expect(result).toEqual(["--ignore", "dist"]); - }); - - it("handles empty array", () => { - const result = serializeIgnoreOptions([]); - expect(result).toEqual([]); - }); -}); - -describe("determineReleaseName", () => { - it("runs `git rev-parse HEAD` with windowsHide so no console window flashes on Windows", () => { - // Clear env so the function falls through the CI/git-provider checks to the - // git fallback (CI runs with GITHUB_SHA set, which would otherwise short-circuit). - const originalEnv = process.env; - process.env = {}; - const execSyncSpy = vi - .spyOn(childProcess, "execSync") - .mockReturnValue(Buffer.from("0".repeat(40))); - - try { - determineReleaseName(); - expect(execSyncSpy).toHaveBeenCalledWith( - "git rev-parse HEAD", - expect.objectContaining({ windowsHide: true }) - ); - } finally { - process.env = originalEnv; - execSyncSpy.mockRestore(); - } - }); -}); diff --git a/packages/bundler-plugins/test/esbuild/public-api.test.ts b/packages/bundler-plugins/test/esbuild/public-api.test.ts deleted file mode 100644 index 04a694e2..00000000 --- a/packages/bundler-plugins/test/esbuild/public-api.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { sentryEsbuildPlugin } from "../../src/esbuild"; -import type { Plugin } from "esbuild"; -import { describe, it, expect, test } from "vitest"; - -test("Esbuild plugin should exist", () => { - expect(sentryEsbuildPlugin).toBeDefined(); - expect(typeof sentryEsbuildPlugin).toBe("function"); -}); - -describe("sentryEsbuildPlugin", () => { - it("returns an esbuild plugin", () => { - const plugin = sentryEsbuildPlugin({ - authToken: "test-token", - org: "test-org", - project: "test-project", - }) as Plugin; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect(plugin).toEqual({ name: "sentry-esbuild-plugin", setup: expect.any(Function) }); - }); -}); diff --git a/packages/bundler-plugins/test/rollup/__snapshots__/public-api.test.ts.snap b/packages/bundler-plugins/test/rollup/__snapshots__/public-api.test.ts.snap deleted file mode 100644 index cd0ad570..00000000 --- a/packages/bundler-plugins/test/rollup/__snapshots__/public-api.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Hooks > renderChunk > should process file 'bundle.cjs' 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b80112c0-6818-486d-96f0-185c023439b4",e._sentryDebugIdIdentifier="sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4");}catch(e){}}();console.log("test");"`; - -exports[`Hooks > renderChunk > should process file 'bundle.js#hash' 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b80112c0-6818-486d-96f0-185c023439b4",e._sentryDebugIdIdentifier="sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4");}catch(e){}}();console.log("test");"`; - -exports[`Hooks > renderChunk > should process file 'bundle.js' 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b80112c0-6818-486d-96f0-185c023439b4",e._sentryDebugIdIdentifier="sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4");}catch(e){}}();console.log("test");"`; - -exports[`Hooks > renderChunk > should process file 'bundle.js?foo=bar' 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b80112c0-6818-486d-96f0-185c023439b4",e._sentryDebugIdIdentifier="sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4");}catch(e){}}();console.log("test");"`; - -exports[`Hooks > renderChunk > should process file 'bundle.mjs' 1`] = `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b80112c0-6818-486d-96f0-185c023439b4",e._sentryDebugIdIdentifier="sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4");}catch(e){}}();console.log("test");"`; diff --git a/packages/bundler-plugins/test/rollup/public-api.test.ts b/packages/bundler-plugins/test/rollup/public-api.test.ts deleted file mode 100644 index 3b4113c0..00000000 --- a/packages/bundler-plugins/test/rollup/public-api.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { sentryRollupPlugin } from "../../src/rollup"; -import type { Plugin, SourceMap } from "rollup"; -import { describe, it, expect, test, beforeEach, vi } from "vitest"; - -test("Rollup plugin should exist", () => { - expect(sentryRollupPlugin).toBeDefined(); - expect(typeof sentryRollupPlugin).toBe("function"); -}); - -describe("sentryRollupPlugin", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("returns an array of rollup plugins (although only one)", () => { - const plugins = sentryRollupPlugin({ - authToken: "test-token", - org: "test-org", - project: "test-project", - }) as Plugin[]; - - expect(Array.isArray(plugins)).toBe(true); - expect(plugins).toHaveLength(1); - - expect(plugins[0]?.name).toBe("sentry-rollup-plugin"); - }); -}); - -describe("Hooks", () => { - const [plugin] = sentryRollupPlugin({ release: { inject: false } }) as [Plugin]; - - const renderChunk = plugin.renderChunk as ( - code: string, - chunkInfo: { fileName: string; facadeModuleId?: string } - ) => { - code: string; - map: SourceMap; - } | null; - - describe("renderChunk", () => { - it("should inject debug ID into clean JavaScript files", () => { - const code = 'console.log("Hello world");'; - const result = renderChunk(code, { fileName: "bundle.js" }); - - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d4309f93-5358-4ae1-bcf0-3813aa590eb5",e._sentryDebugIdIdentifier="sentry-dbid-d4309f93-5358-4ae1-bcf0-3813aa590eb5");}catch(e){}}();console.log("Hello world");"` - ); - }); - - it("should inject debug ID after 'use strict'", () => { - const code = '"use strict";\nconsole.log("Hello world");'; - const result = renderChunk(code, { fileName: "bundle.js" }); - - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot(` - ""use strict";!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="79a86c07-8ecc-4367-82b0-88cf822f2d41",e._sentryDebugIdIdentifier="sentry-dbid-79a86c07-8ecc-4367-82b0-88cf822f2d41");}catch(e){}}(); - console.log("Hello world");" - `); - }); - - it.each([ - ["bundle.js"], - ["bundle.mjs"], - ["bundle.cjs"], - ["bundle.js?foo=bar"], - ["bundle.js#hash"], - ])("should process file '%s'", (fileName) => { - const code = 'console.log("test");'; - const result = renderChunk(code, { fileName }); - - expect(result).not.toBeNull(); - expect(result?.code).toMatchSnapshot(); - }); - - it.each([["index.html"], ["styles.css"]])("should NOT process file '%s': %s", (fileName) => { - const code = 'console.log("test");'; - const result = renderChunk(code, { fileName }); - - expect(result).toBeNull(); - }); - - it.each([ - [ - "inline format at start", - ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};console.log("test");', - ], - [ - "comment format at end", - 'console.log("test");\n//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n//# sourceMappingURL=bundle.js.map', - ], - [ - "inline format with large file", - `"use strict";\n${"// comment\n".repeat(10)};{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};${`\nconsole.log("line");\n`.repeat(100)}`, - ], - ])("should NOT inject when debug ID already exists (%s)", (_description, code) => { - const result = renderChunk(code, { fileName: "bundle.js" }); - expect(result).toBeNull(); - }); - - it("should only check boundaries for performance (not entire file)", () => { - // Inline format beyond first 6KB boundary - const codeWithInlineBeyond6KB = `${"a".repeat(6100)};{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};`; - - expect(renderChunk(codeWithInlineBeyond6KB, { fileName: "bundle.js" })).not.toBeNull(); - - // Comment format beyond last 500 bytes boundary - const codeWithCommentBeyond500B = `//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n${"a".repeat(600)}`; - - expect(renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" })).not.toBeNull(); - }); - - describe("HTML facade chunks (MPA vs SPA)", () => { - // Issue #829: MPA facades should be skipped - // Regression fix: SPA main bundles with HTML facades should NOT be skipped - - it.each([ - ["empty", ""], - ["only side-effect imports", `import './shared-module.js';`], - ["only named imports", `import { foo, bar } from './shared-module.js';`], - ["only re-exports", `export * from './shared-module.js';`], - [ - "multiple imports and comments", - `// This is a facade module -import './moduleA.js'; -import { x } from './moduleB.js'; -/* block comment */ -export * from './moduleC.js';`, - ], - ["'use strict' and imports only", `"use strict";\nimport './shared-module.js';`], - ["query string in facadeModuleId", `import './shared.js';`, "?query=param"], - ["hash in facadeModuleId", `import './shared.js';`, "#hash"], - ])("should SKIP HTML facade chunks: %s", (_, code, suffix = "") => { - const result = renderChunk(code, { - fileName: "page1.js", - facadeModuleId: `/path/to/page1.html${suffix}`, - }); - expect(result).toBeNull(); - }); - - it("should inject into HTML facade with function declarations", () => { - const result = renderChunk(`function main() { console.log("hello"); }`, { - fileName: "index.js", - facadeModuleId: "/path/to/index.html", - }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c4c89e04-3658-4874-b25b-07e638185091",e._sentryDebugIdIdentifier="sentry-dbid-c4c89e04-3658-4874-b25b-07e638185091");}catch(e){}}();function main() { console.log("hello"); }"` - ); - }); - - it("should inject into HTML facade with variable declarations", () => { - const result = renderChunk(`const x = 42;`, { - fileName: "index.js", - facadeModuleId: "/path/to/index.html", - }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="43e69766-1963-49f2-a291-ff8de60cc652",e._sentryDebugIdIdentifier="sentry-dbid-43e69766-1963-49f2-a291-ff8de60cc652");}catch(e){}}();const x = 42;"` - ); - }); - - it("should inject into HTML facade with substantial code (SPA main bundle)", () => { - const code = `import { initApp } from './app.js'; - -const config = { debug: true }; - -function bootstrap() { - initApp(config); -} - -bootstrap();`; - const result = renderChunk(code, { - fileName: "index.js", - facadeModuleId: "/path/to/index.html", - }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot(` - "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d0c4524b-496e-45a4-9852-7558d043ba3c",e._sentryDebugIdIdentifier="sentry-dbid-d0c4524b-496e-45a4-9852-7558d043ba3c");}catch(e){}}();import { initApp } from './app.js'; - - const config = { debug: true }; - - function bootstrap() { - initApp(config); - } - - bootstrap();" - `); - }); - - it("should inject into HTML facade with mixed imports and code", () => { - const result = renderChunk( - `import './polyfills.js';\nimport { init } from './app.js';\n\ninit();`, - { fileName: "index.js", facadeModuleId: "/path/to/index.html" } - ); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot(` - "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175",e._sentryDebugIdIdentifier="sentry-dbid-28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175");}catch(e){}}();import './polyfills.js'; - import { init } from './app.js'; - - init();" - `); - }); - - it("should inject into regular JS chunks (no HTML facade)", () => { - const result = renderChunk(`console.log("Hello");`, { fileName: "bundle.js" }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="79f18a7f-ca16-4168-9797-906c82058367",e._sentryDebugIdIdentifier="sentry-dbid-79f18a7f-ca16-4168-9797-906c82058367");}catch(e){}}();console.log("Hello");"` - ); - }); - }); - }); -}); diff --git a/packages/bundler-plugins/test/tsconfig.json b/packages/bundler-plugins/test/tsconfig.json deleted file mode 100644 index 76d0c9cf..00000000 --- a/packages/bundler-plugins/test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "../tsconfig.json", - "include": ["../src/**/*", "./**/*"], - "compilerOptions": { - "types": ["node"] - } -} diff --git a/packages/bundler-plugins/test/vite/public-api.test.ts b/packages/bundler-plugins/test/vite/public-api.test.ts deleted file mode 100644 index 43e93467..00000000 --- a/packages/bundler-plugins/test/vite/public-api.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { sentryVitePlugin } from "../../src/vite"; -import { describe, it, expect, test, beforeEach, vi } from "vitest"; - -test("Vite plugin should exist", () => { - expect(sentryVitePlugin).toBeDefined(); - expect(typeof sentryVitePlugin).toBe("function"); -}); - -describe("sentryVitePlugin", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("returns an array of Vite plugins", () => { - const plugins = sentryVitePlugin({ - authToken: "test-token", - org: "test-org", - project: "test-project", - }); - - expect(Array.isArray(plugins)).toBe(true); - - const pluginNames = plugins.map((plugin) => plugin.name); - - expect(pluginNames).toEqual(expect.arrayContaining(["sentry-vite-plugin"])); - }); - - it("returns an array of Vite pluginswhen unplugin returns a single plugin", () => { - const plugins = sentryVitePlugin({ - authToken: "test-token", - org: "test-org", - project: "test-project", - disable: true, // This causes unplugin to return only the noop plugin - }); - - expect(Array.isArray(plugins)).toBe(true); - expect(plugins.length).toBeGreaterThanOrEqual(1); - expect(plugins[0]).toHaveProperty("name"); - }); -}); diff --git a/packages/bundler-plugins/test/webpack/public-api.test.ts b/packages/bundler-plugins/test/webpack/public-api.test.ts deleted file mode 100644 index 098e48f2..00000000 --- a/packages/bundler-plugins/test/webpack/public-api.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { WebpackPluginInstance } from "webpack"; -import { sentryWebpackPlugin } from "../../src/webpack"; -import { describe, it, expect, test } from "vitest"; - -test("Webpack plugin should exist", () => { - expect(sentryWebpackPlugin).toBeDefined(); - expect(typeof sentryWebpackPlugin).toBe("function"); -}); - -describe("sentryWebpackPlugin", () => { - it("returns a webpack plugin", () => { - const plugin = sentryWebpackPlugin({ - authToken: "test-token", - org: "test-org", - project: "test-project", - }) as WebpackPluginInstance; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect(plugin).toEqual({ apply: expect.any(Function) }); - }); -}); diff --git a/packages/bundler-plugins/test/webpack/webpack5.test.ts b/packages/bundler-plugins/test/webpack/webpack5.test.ts deleted file mode 100644 index b6ab5d8d..00000000 --- a/packages/bundler-plugins/test/webpack/webpack5.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { WebpackPluginInstance } from "webpack"; -import { sentryWebpackPlugin } from "../../src/webpack/index"; -import { describe, it, expect, test } from "vitest"; - -test("Webpack plugin should exist", () => { - expect(sentryWebpackPlugin).toBeDefined(); - expect(typeof sentryWebpackPlugin).toBe("function"); -}); - -describe("sentryWebpackPlugin", () => { - it("returns a webpack plugin", () => { - const plugin = sentryWebpackPlugin({ - authToken: "test-token", - org: "test-org", - project: "test-project", - }) as WebpackPluginInstance; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect(plugin).toEqual({ apply: expect.any(Function) }); - }); -}); diff --git a/packages/bundler-plugins/tsconfig.json b/packages/bundler-plugins/tsconfig.json deleted file mode 100644 index 088fbb9b..00000000 --- a/packages/bundler-plugins/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.json", - "include": ["./src/**/*.ts", "./package.json"], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/packages/bundler-plugins/types.tsconfig.json b/packages/bundler-plugins/types.tsconfig.json deleted file mode 100644 index e427dd96..00000000 --- a/packages/bundler-plugins/types.tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "include": ["./src/**/*"], - "compilerOptions": { - "rootDir": "./src", - "declaration": true, - "emitDeclarationOnly": true, - "declarationDir": "./dist/types" - } -} diff --git a/packages/esbuild-plugin/package.json b/packages/esbuild-plugin/package.json index 153bdb37..f264ea59 100644 --- a/packages/esbuild-plugin/package.json +++ b/packages/esbuild-plugin/package.json @@ -46,7 +46,7 @@ "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugins": "5.3.0" + "@sentry/bundler-plugins": "^10.62.0" }, "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", diff --git a/packages/integration-tests-next/fixtures/esbuild/package.json b/packages/integration-tests-next/fixtures/esbuild/package.json index b6f09666..ae9341e2 100644 --- a/packages/integration-tests-next/fixtures/esbuild/package.json +++ b/packages/integration-tests-next/fixtures/esbuild/package.json @@ -9,7 +9,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/esbuild-plugin": "file:../../../esbuild-plugin/sentry-esbuild-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/esbuild/telemetry.test.ts b/packages/integration-tests-next/fixtures/esbuild/telemetry.test.ts index d70b5932..6e1314e0 100644 --- a/packages/integration-tests-next/fixtures/esbuild/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/esbuild/telemetry.test.ts @@ -5,9 +5,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"esbuild","bundler-major-version":"28"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"esbuild","bundler-major-version":"28"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", "telemetry.js": "(() => { // _sentry-injection-stub diff --git a/packages/integration-tests-next/fixtures/rolldown/package.json b/packages/integration-tests-next/fixtures/rolldown/package.json index 43b95341..1aec87ac 100644 --- a/packages/integration-tests-next/fixtures/rolldown/package.json +++ b/packages/integration-tests-next/fixtures/rolldown/package.json @@ -10,7 +10,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/rolldown/telemetry.test.ts b/packages/integration-tests-next/fixtures/rolldown/telemetry.test.ts index 1e5c06d8..1c950418 100644 --- a/packages/integration-tests-next/fixtures/rolldown/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/rolldown/telemetry.test.ts @@ -17,9 +17,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); //#endregion ", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"3"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"3"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/fixtures/rollup3/package.json b/packages/integration-tests-next/fixtures/rollup3/package.json index aa29336e..53cd21e6 100644 --- a/packages/integration-tests-next/fixtures/rollup3/package.json +++ b/packages/integration-tests-next/fixtures/rollup3/package.json @@ -12,7 +12,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/rollup3/telemetry.test.ts b/packages/integration-tests-next/fixtures/rollup3/telemetry.test.ts index 2a108cc6..439da895 100644 --- a/packages/integration-tests-next/fixtures/rollup3/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/rollup3/telemetry.test.ts @@ -8,9 +8,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"3"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"3"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/fixtures/rollup4/package.json b/packages/integration-tests-next/fixtures/rollup4/package.json index 5abcc765..944d4479 100644 --- a/packages/integration-tests-next/fixtures/rollup4/package.json +++ b/packages/integration-tests-next/fixtures/rollup4/package.json @@ -12,7 +12,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/rollup4/telemetry.test.ts b/packages/integration-tests-next/fixtures/rollup4/telemetry.test.ts index bb27238e..fb2b5632 100644 --- a/packages/integration-tests-next/fixtures/rollup4/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/rollup4/telemetry.test.ts @@ -8,9 +8,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/fixtures/vite4/package.json b/packages/integration-tests-next/fixtures/vite4/package.json index cd6089fc..0a10b4e9 100644 --- a/packages/integration-tests-next/fixtures/vite4/package.json +++ b/packages/integration-tests-next/fixtures/vite4/package.json @@ -11,7 +11,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite4/telemetry.test.ts b/packages/integration-tests-next/fixtures/vite4/telemetry.test.ts index 560fd499..463affa5 100644 --- a/packages/integration-tests-next/fixtures/vite4/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/vite4/telemetry.test.ts @@ -16,9 +16,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { }(); console.log("hello world"); ", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/fixtures/vite6/package.json b/packages/integration-tests-next/fixtures/vite6/package.json index adf5500e..038495ef 100644 --- a/packages/integration-tests-next/fixtures/vite6/package.json +++ b/packages/integration-tests-next/fixtures/vite6/package.json @@ -9,7 +9,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite7/package.json b/packages/integration-tests-next/fixtures/vite7/package.json index 14b5d21e..9a7ea880 100644 --- a/packages/integration-tests-next/fixtures/vite7/package.json +++ b/packages/integration-tests-next/fixtures/vite7/package.json @@ -11,7 +11,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite7/telemetry.test.ts b/packages/integration-tests-next/fixtures/vite7/telemetry.test.ts index 196ff3ab..6941b7b5 100644 --- a/packages/integration-tests-next/fixtures/vite7/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/vite7/telemetry.test.ts @@ -16,9 +16,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { })(); console.log("hello world"); ", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"7"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"7"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/fixtures/vite8/package.json b/packages/integration-tests-next/fixtures/vite8/package.json index 0ee48e39..6843a41a 100644 --- a/packages/integration-tests-next/fixtures/vite8/package.json +++ b/packages/integration-tests-next/fixtures/vite8/package.json @@ -11,7 +11,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite8/telemetry.test.ts b/packages/integration-tests-next/fixtures/vite8/telemetry.test.ts index 31b79924..a180f6a4 100644 --- a/packages/integration-tests-next/fixtures/vite8/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/vite8/telemetry.test.ts @@ -17,9 +17,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); //#endregion ", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"8"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"8"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/fixtures/webpack5/package.json b/packages/integration-tests-next/fixtures/webpack5/package.json index 20bf49a2..7b433701 100644 --- a/packages/integration-tests-next/fixtures/webpack5/package.json +++ b/packages/integration-tests-next/fixtures/webpack5/package.json @@ -12,7 +12,6 @@ }, "pnpm": { "overrides": { - "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/webpack-plugin": "file:../../../webpack-plugin/sentry-webpack-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/webpack5/telemetry.test.ts b/packages/integration-tests-next/fixtures/webpack5/telemetry.test.ts index 4644423c..c0430411 100644 --- a/packages/integration-tests-next/fixtures/webpack5/telemetry.test.ts +++ b/packages/integration-tests-next/fixtures/webpack5/telemetry.test.ts @@ -13,9 +13,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { /******/ })() ;", - "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"webpack","bundler-major-version":"5"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.56.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.56.0"}]}}]]], - [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.56.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"1"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.source":"custom","sentry.sample_rate":1},"origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"webpack","bundler-major-version":"5"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"10.62.0","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"10.62.0"}]}}]]], + [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"10.62.0"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } `); diff --git a/packages/integration-tests-next/package.json b/packages/integration-tests-next/package.json index b7afcc9d..0228a6f4 100644 --- a/packages/integration-tests-next/package.json +++ b/packages/integration-tests-next/package.json @@ -18,6 +18,7 @@ "@sentry/webpack-plugin": "5.3.0" }, "devDependencies": { + "@babel/preset-react": "^7.23.3", "premove": "^4.0.0", "vitest": "^4.0.0" }, diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index 8bfa911a..2f90b09e 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -47,7 +47,7 @@ "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugins": "5.3.0" + "@sentry/bundler-plugins": "^10.62.0" }, "peerDependencies": { "rollup": ">=3.2.0" diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 16075874..6426a9d0 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -46,7 +46,7 @@ "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugins": "5.3.0" + "@sentry/bundler-plugins": "^10.62.0" }, "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index d3677ac3..5a6b4eed 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -51,7 +51,7 @@ "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugins": "5.3.0" + "@sentry/bundler-plugins": "^10.62.0" }, "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", diff --git a/yarn.lock b/yarn.lock index fab98a70..e7673525 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,15 @@ "@babel/highlight" "^7.23.4" chalk "^2.4.2" +"@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" @@ -54,12 +63,23 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== +"@babel/generator@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" + integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== dependencies: - "@babel/types" "^7.22.5" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz#c70fe3c6ecbdc3fd2dd1b0f498428b88b82ce47f" + integrity sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw== + dependencies: + "@babel/types" "^7.29.7" "@babel/helper-compilation-targets@^7.23.6": version "7.23.6" @@ -85,6 +105,11 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.23.0" +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -99,6 +124,14 @@ dependencies: "@babel/types" "^7.22.15" +"@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helper-module-transforms@^7.23.3": version "7.23.3" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" @@ -110,10 +143,10 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.20" -"@babel/helper-plugin-utils@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== +"@babel/helper-plugin-utils@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== "@babel/helper-simple-access@^7.22.5": version "7.22.5" @@ -134,16 +167,31 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5": +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + +"@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + "@babel/helpers@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz#a3dd462b41769c95db8091e49cfe019389a9409b" @@ -162,62 +210,69 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.0": +"@babel/parser@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== -"@babel/plugin-syntax-jsx@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" - integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== +"@babel/parser@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" + integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/types" "^7.29.7" -"@babel/plugin-transform-react-display-name@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz#70529f034dd1e561045ad3c8152a267f0d7b6200" - integrity sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw== +"@babel/plugin-syntax-jsx@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz#622c16f9ad63782fe6e83dadc7e40330744b7f1e" + integrity sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.29.7" -"@babel/plugin-transform-react-jsx-development@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" - integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== +"@babel/plugin-transform-react-display-name@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz#bf161a6d750267b79db7ff6f8fb89c3369b02df3" + integrity sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q== dependencies: - "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/helper-plugin-utils" "^7.29.7" -"@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5": - version "7.23.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312" - integrity sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA== +"@babel/plugin-transform-react-jsx-development@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz#64e6aacb5cb43b9e80d3d5f19ddefc158a624f09" + integrity sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.23.3" - "@babel/types" "^7.23.4" + "@babel/plugin-transform-react-jsx" "^7.29.7" -"@babel/plugin-transform-react-pure-annotations@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz#fabedbdb8ee40edf5da96f3ecfc6958e3783b93c" - integrity sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ== +"@babel/plugin-transform-react-jsx@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz#3d16a0e5773f079400a8c82a190709cdf92ee204" + integrity sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-syntax-jsx" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/plugin-transform-react-pure-annotations@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz#76445c90112dd0a7371b63264563bfa9a4fcd6e3" + integrity sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" "@babel/preset-react@^7.23.3": - version "7.23.3" - resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz#f73ca07e7590f977db07eb54dbe46538cc015709" - integrity sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w== + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz#2ed18366e38c2081bbf1760dc01e88fa5674eb17" + integrity sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-transform-react-display-name" "^7.23.3" - "@babel/plugin-transform-react-jsx" "^7.22.15" - "@babel/plugin-transform-react-jsx-development" "^7.22.5" - "@babel/plugin-transform-react-pure-annotations" "^7.23.3" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + "@babel/plugin-transform-react-display-name" "^7.29.7" + "@babel/plugin-transform-react-jsx" "^7.29.7" + "@babel/plugin-transform-react-jsx-development" "^7.29.7" + "@babel/plugin-transform-react-pure-annotations" "^7.29.7" "@babel/template@^7.22.15", "@babel/template@^7.24.0": version "7.24.0" @@ -228,6 +283,15 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/traverse@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" @@ -244,7 +308,20 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.3.0": +"@babel/traverse@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" + integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + debug "^4.3.1" + +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== @@ -253,6 +330,14 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" + integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@emnapi/core@1.10.0", "@emnapi/core@^1.1.0": version "1.10.0" resolved "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" @@ -558,11 +643,24 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.13" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -581,7 +679,7 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.5.5": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== @@ -594,6 +692,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@napi-rs/wasm-runtime@0.2.4": version "0.2.4" resolved "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz#d27788176f250d86e498081e3c5ff48a17606918" @@ -1114,6 +1220,19 @@ resolved "https://registry.npmjs.org/@sentry-internal/typescript/-/typescript-10.53.1.tgz#216b3e3ec0d5aa7ef3e7055ea50d6371d9786cce" integrity sha512-7ncY4Ww9MsTf5lsX9qAUaiNeQdKNBeZm7rtFze0UeU8fTLxOKx9ink4jCFa1xB/sztIR1pq/rGmnBs7evvn+aw== +"@sentry/bundler-plugins@^10.62.0": + version "10.62.0" + resolved "https://registry.npmjs.org/@sentry/bundler-plugins/-/bundler-plugins-10.62.0.tgz#cc56c975791a0c97d26259f779024228f0625816" + integrity sha512-4O93t2zyVg6XHSEbTxjScB8IlnyKmN5irkhabyT5CuAjK70YirO24J4fEhSLthGGaEg90hYsyBQn6ObD56xhQA== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/cli" "^2.58.6" + "@sentry/core" "10.62.0" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^13.0.6" + magic-string "~0.30.8" + "@sentry/cli-darwin@2.58.6": version "2.58.6" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.58.6.tgz#38fd82751014b287e58e99ef948d01ca1e09f41d" @@ -1174,10 +1293,10 @@ "@sentry/cli-win32-i686" "2.58.6" "@sentry/cli-win32-x64" "2.58.6" -"@sentry/core@10.56.0": - version "10.56.0" - resolved "https://registry.npmjs.org/@sentry/core/-/core-10.56.0.tgz#61159a5879d7937b6509cf4e6a680b991ef82001" - integrity sha512-L+u1dIz5SANrmST5jhIwETtt4apILgKrylv12X4hKJU0PvZl+NorjeV/ty3MwzpKQPg6b6q6qMOSLc1rLpy3iQ== +"@sentry/core@10.62.0": + version "10.62.0" + resolved "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz#73a4a8bcdded6d741d6cf58bfd055c1b60b3fe10" + integrity sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A== "@sentry/core@7.50.0": version "7.50.0" @@ -1212,13 +1331,6 @@ lru_map "^0.3.3" tslib "^1.9.3" -"@sentry/types@10.56.0": - version "10.56.0" - resolved "https://registry.npmjs.org/@sentry/types/-/types-10.56.0.tgz#4add2d3cfcb294e291e6a63bd18a1f26d216cf1b" - integrity sha512-I9JtS/EtzpV5o9MwLNOKBCTmGU61HRT3E9MS9k68cKxAwexNH2zFW0E9EnQYLDhANXsvHA6moGA+nqt2rbD92Q== - dependencies: - "@sentry/core" "10.56.0" - "@sentry/types@7.50.0": version "7.50.0" resolved "https://registry.npmjs.org/@sentry/types/-/types-7.50.0.tgz#52a035cad83a80ca26fa53c09eb1241250c3df3e" @@ -1256,39 +1368,6 @@ dependencies: tslib "^2.4.0" -"@types/babel__core@^7.20.5": - version "7.20.5" - resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.1" - resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.18.5" - resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" - integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== - dependencies: - "@babel/types" "^7.3.0" - "@types/body-parser@*": version "1.19.2" resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -3112,6 +3191,11 @@ jsesc@^2.5.1: resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"