diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 74c3a25..2b15aaa 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,6 +21,7 @@ module.exports = { rules: { '@sentry-internal/sdk/no-nullish-coalescing': 'off', '@sentry-internal/sdk/no-optional-chaining': 'off', + '@sentry-internal/sdk/no-unsupported-es6-methods': 'off', }, }, { diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e0e0d6..7ee9a27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: token: ${{ secrets.GH_RELEASE_PAT }} fetch-depth: 0 - name: Install Dependencies - run: yarn install + run: pnpm install - name: Prepare release uses: getsentry/action-prepare-release@lms/write-config-branch env: diff --git a/README.md b/README.md index e68f323..91375d1 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,13 @@ You can run `npx @sentry/migr8 --help` to get a list of available options. ## Transformations +### Use functional integrations instead of integration classes + +This updates usage of class-based integrations to the new, functional style. For example: + +- `new BrowserTracing()` → `browserTracingIntegration()` +- `new Sentry.Replay()` → `Sentry.replayIntegration()` + ### Replay Config v7>v8 This migrates deprecated replay configuration from v7 to v8. This includes: diff --git a/src/transformers/integrations/index.js b/src/transformers/integrations/index.js new file mode 100644 index 0000000..91c6209 --- /dev/null +++ b/src/transformers/integrations/index.js @@ -0,0 +1,16 @@ +import path from 'path'; +import url from 'url'; + +import { runJscodeshift } from '../../utils/jscodeshift.js'; + +/** + * @type {import('types').Transformer} + */ +export default { + name: 'Use functional integrations instead of integration classes', + transform: async (files, options) => { + const transformPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), './transform.cjs'); + + await runJscodeshift(transformPath, files, options); + }, +}; diff --git a/src/transformers/integrations/integrations.test.js b/src/transformers/integrations/integrations.test.js new file mode 100644 index 0000000..e9885ad --- /dev/null +++ b/src/transformers/integrations/integrations.test.js @@ -0,0 +1,356 @@ +import { afterEach, describe, it } from 'node:test'; +import { rmSync } from 'node:fs'; +import assert from 'node:assert'; + +import { getDirFileContent, getFixturePath, makeTmpDir } from '../../../test-helpers/testPaths.js'; +import { assertStringEquals } from '../../../test-helpers/assert.js'; + +import integrationsTransformer from './index.js'; + +describe('transformers | integrations', () => { + let tmpDir = ''; + + afterEach(() => { + if (tmpDir) { + rmSync(tmpDir, { force: true, recursive: true }); + tmpDir = ''; + } + }); + + it('has correct name', () => { + assert.equal(integrationsTransformer.name, 'Use functional integrations instead of integration classes'); + }); + + it('works with app without Sentry', async () => { + tmpDir = makeTmpDir(getFixturePath('noSentry')); + await integrationsTransformer.transform([tmpDir], { filePatterns: [] }); + + const actual1 = getDirFileContent(tmpDir, 'app.js'); + assert.equal(actual1, getDirFileContent(`${process.cwd()}/test-fixtures/noSentry`, 'app.js')); + }); + + it('works with example files', async () => { + tmpDir = makeTmpDir(getFixturePath('integrations')); + await integrationsTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/browser' }); + + const withImports = getDirFileContent(tmpDir, 'withImports.js'); + const withImportsTs = getDirFileContent(tmpDir, 'withImports.ts'); + const withRequire = getDirFileContent(tmpDir, 'withRequire.js'); + const dedupeImports = getDirFileContent(tmpDir, 'dedupeImports.js'); + const dedupeRequire = getDirFileContent(tmpDir, 'dedupeRequire.js'); + const simpleImport = getDirFileContent(tmpDir, 'simpleImport.js'); + const simpleNamespaceImport = getDirFileContent(tmpDir, 'simpleNamespaceImport.js'); + const simpleRequire = getDirFileContent(tmpDir, 'simpleRequire.js'); + const simpleNamespaceRequire = getDirFileContent(tmpDir, 'simpleNamespaceRequire.js'); + const simpleImportIntegrations = getDirFileContent(tmpDir, 'simpleImportIntegrations.js'); + const simpleRequireIntegrations = getDirFileContent(tmpDir, 'simpleRequireIntegrations.js'); + const integrationsFunctionalImport = getDirFileContent(tmpDir, 'integrationsFunctionalImport.js'); + + assertStringEquals( + withImports, + `import * as Sentry from '@sentry/browser'; +import * as SentryIntegrations from '@sentry/integrations'; +import { httpClientIntegration } from '@sentry/integrations'; + +function orig() { + // do something +} + +function doSomething() { + // Check different invocations + const a = Sentry.browserTracingIntegration(); + const b = Sentry.browserTracingIntegration({ option: 'value' }); + const c = Sentry.browserTracingIntegration({ option: 'value' }); + const d = Sentry.browserTracingIntegration({ option: 'value' }); + const e = Sentry.breadcrumbsIntegration({ option: 'value' }); + const f = Sentry.captureConsoleIntegration({ option: 'value' }); + const g = Sentry.contextLinesIntegration(); + const h = new Sentry.SomethingElse.Span(); + + const integrations = [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + Sentry.feedbackIntegration(), + Sentry.breadcrumbsIntegration(), + Sentry.browserApiErrorsIntegration(), + Sentry.globalHandlersIntegration(), + Sentry.httpContextIntegration(), + Sentry.inboundFiltersIntegration(), + Sentry.functionToStringIntegration(), + Sentry.linkedErrorsIntegration(), + Sentry.moduleMetadataIntegration(), + Sentry.requestDataIntegration(), + SentryIntegrations.httpClientIntegration(), + httpClientIntegration(), + SentryIntegrations.captureConsoleIntegration(), + SentryIntegrations.debugIntegration(), + SentryIntegrations.dedupeIntegration(), + SentryIntegrations.extraErrorDataIntegration(), + SentryIntegrations.reportingObserverIntegration(), + SentryIntegrations.rewriteFramesIntegration(), + SentryIntegrations.sessionTimingIntegration(), + SentryIntegrations.contextLinesIntegration(), + Sentry.consoleIntegration(), + Sentry.httpIntegration(), + Sentry.onUncaughtExceptionIntegration(), + Sentry.onUnhandledRejectionIntegration(), + Sentry.modulesIntegration(), + Sentry.contextLinesIntegration(), + Sentry.nodeContextIntegration(), + Sentry.localVariablesIntegration(), + Sentry.nodeFetchIntegration(), + Sentry.spotlightIntegration(), + Sentry.anrIntegration(), + Sentry.hapiIntegration(), + ]; + + // Other classes are ignored + const x = new MyClass(); + const y = new Sentry.Span(); + const z = new SentryIntegrations.MyIntegration(); +}` + ); + + assertStringEquals( + withImportsTs, + `import * as Sentry from '@sentry/browser'; +import * as SentryIntegrations from '@sentry/integrations'; +import { httpClientIntegration } from '@sentry/integrations'; + +function orig(): void { + // do something +} + +function doSomething(): void { + // Check different invocations + const a = Sentry.browserTracingIntegration(); + const b = Sentry.browserTracingIntegration({ option: 'value' }); + const c = Sentry.browserTracingIntegration({ option: 'value' }); + const d = Sentry.browserTracingIntegration({ option: 'value' }); + const e = Sentry.breadcrumbsIntegration({ option: 'value' }); + const f = Sentry.captureConsoleIntegration({ option: 'value' }); + const g = Sentry.contextLinesIntegration(); + const h = new Sentry.SomethingElse.Span(); + + const integrations = [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + Sentry.feedbackIntegration(), + Sentry.breadcrumbsIntegration(), + Sentry.browserApiErrorsIntegration(), + Sentry.globalHandlersIntegration(), + Sentry.httpContextIntegration(), + Sentry.inboundFiltersIntegration(), + Sentry.functionToStringIntegration(), + Sentry.linkedErrorsIntegration(), + Sentry.moduleMetadataIntegration(), + Sentry.requestDataIntegration(), + SentryIntegrations.httpClientIntegration(), + httpClientIntegration(), + SentryIntegrations.captureConsoleIntegration(), + SentryIntegrations.debugIntegration(), + SentryIntegrations.dedupeIntegration(), + SentryIntegrations.extraErrorDataIntegration(), + SentryIntegrations.reportingObserverIntegration(), + SentryIntegrations.rewriteFramesIntegration(), + SentryIntegrations.sessionTimingIntegration(), + SentryIntegrations.contextLinesIntegration(), + Sentry.consoleIntegration(), + Sentry.httpIntegration(), + Sentry.onUncaughtExceptionIntegration(), + Sentry.onUnhandledRejectionIntegration(), + Sentry.modulesIntegration(), + Sentry.contextLinesIntegration(), + Sentry.nodeContextIntegration(), + Sentry.localVariablesIntegration(), + Sentry.nodeFetchIntegration(), + Sentry.spotlightIntegration(), + Sentry.anrIntegration(), + Sentry.hapiIntegration(), + ]; + + // Other classes are ignored + const x = new MyClass(); + const y = new Sentry.Span(); + const z = new SentryIntegrations.MyIntegration(); +}` + ); + + assertStringEquals( + withRequire, + `const { + browserTracingIntegration, + breadcrumbsIntegration, + captureConsoleIntegration +} = require('@sentry/browser'); +const Sentry = require('@sentry/browser'); +const SentryIntegrations = require('@sentry/integrations'); +const { httpClientIntegration } = require('@sentry/integrations'); + +function orig() { + // do something +} + +function doSomething() { + // Check different invocations + const a = browserTracingIntegration(); + const b = browserTracingIntegration({ option: 'value' }); + const c = Sentry.browserTracingIntegration({ option: 'value' }); + const d = browserTracingIntegration({ option: 'value' }); + const e = breadcrumbsIntegration({ option: 'value' }); + const f = captureConsoleIntegration({ option: 'value' }); + const g = Sentry.contextLinesIntegration(); + const h = new Sentry.SomethingElse.Span(); + + const integrations = [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + Sentry.feedbackIntegration(), + Sentry.breadcrumbsIntegration(), + Sentry.browserApiErrorsIntegration(), + Sentry.globalHandlersIntegration(), + Sentry.httpContextIntegration(), + Sentry.inboundFiltersIntegration(), + Sentry.functionToStringIntegration(), + Sentry.linkedErrorsIntegration(), + Sentry.moduleMetadataIntegration(), + Sentry.requestDataIntegration(), + SentryIntegrations.httpClientIntegration(), + httpClientIntegration(), + SentryIntegrations.captureConsoleIntegration(), + SentryIntegrations.debugIntegration(), + SentryIntegrations.dedupeIntegration(), + SentryIntegrations.extraErrorDataIntegration(), + SentryIntegrations.reportingObserverIntegration(), + SentryIntegrations.rewriteFramesIntegration(), + SentryIntegrations.sessionTimingIntegration(), + SentryIntegrations.contextLinesIntegration(), + Sentry.consoleIntegration(), + Sentry.httpIntegration(), + Sentry.onUncaughtExceptionIntegration(), + Sentry.onUnhandledRejectionIntegration(), + Sentry.modulesIntegration(), + Sentry.contextLinesIntegration(), + Sentry.nodeContextIntegration(), + Sentry.localVariablesIntegration(), + Sentry.nodeFetchIntegration(), + Sentry.spotlightIntegration(), + Sentry.anrIntegration(), + Sentry.hapiIntegration(), + ]; + + // Other classes are ignored + const x = new MyClass(); + const y = new Sentry.Span(); + const z = new SentryIntegrations.MyIntegration(); +}` + ); + + assertStringEquals( + dedupeImports, + `import { browserTracingIntegration, breadcrumbsIntegration } from '@sentry/browser'; + +function doSomething() { + const a = browserTracingIntegration(); + const b = browserTracingIntegration(); + const c = breadcrumbsIntegration(); + const d = breadcrumbsIntegration(); +}` + ); + + assertStringEquals( + dedupeRequire, + `const { + browserTracingIntegration, + breadcrumbsIntegration +} = require('@sentry/browser'); + +function doSomething() { + const a = browserTracingIntegration(); + const b = browserTracingIntegration(); + const c = breadcrumbsIntegration(); + const d = breadcrumbsIntegration(); +}` + ); + + assertStringEquals( + simpleImport, + `import { init, browserTracingIntegration } from '@sentry/browser'; + +function doSomething() { + init({ + integrations: [browserTracingIntegration()] + }); +}` + ); + + assertStringEquals( + simpleNamespaceImport, + `import * as Sentry from '@sentry/browser'; + +function doSomething() { + Sentry.init({ + integrations: [Sentry.browserTracingIntegration()] + }); +}` + ); + + assertStringEquals( + simpleRequire, + `const { init, browserTracingIntegration } = require('@sentry/browser'); + +function doSomething() { + init({ + integrations: [browserTracingIntegration()] + }); +}` + ); + + assertStringEquals( + simpleNamespaceRequire, + `const Sentry = require('@sentry/browser'); + +function doSomething() { + Sentry.init({ + integrations: [Sentry.browserTracingIntegration()] + }); +}` + ); + + assertStringEquals( + simpleImportIntegrations, + `import * as Sentry from '@sentry/browser'; +import { httpClientIntegration } from '@sentry/integrations'; + +function doSomething() { + Sentry.init({ + integrations: [httpClientIntegration()] + }); +}` + ); + + assertStringEquals( + simpleRequireIntegrations, + `const Sentry = require('@sentry/browser'); +const { httpClientIntegration } = require('@sentry/integrations'); + +function doSomething() { + Sentry.init({ + integrations: [httpClientIntegration()] + }); +}` + ); + + assertStringEquals( + integrationsFunctionalImport, + `import * as Sentry from '@sentry/browser'; +import { httpClientIntegration } from '@sentry/integrations'; + +function doSomething() { + Sentry.init({ + integrations: [httpClientIntegration()] + }); +}` + ); + }); +}); diff --git a/src/transformers/integrations/transform.cjs b/src/transformers/integrations/transform.cjs new file mode 100644 index 0000000..9aa439d --- /dev/null +++ b/src/transformers/integrations/transform.cjs @@ -0,0 +1,322 @@ +const { hasSentryImportOrRequire, replaceImported, dedupeImportStatements } = require('../../utils/jscodeshift.cjs'); +const { wrapJscodeshift } = require('../../utils/dom.cjs'); + +const INTEGRATIONS_PACKAGE = '@sentry/integrations'; +const INTEGRATIONS_HASH_KEY = 'Integrations'; + +/** @type Record */ +const integrationMap = { + // Browser + BrowserTracing: 'browserTracingIntegration', + Replay: 'replayIntegration', + Feedback: 'feedbackIntegration', + Breadcrumbs: 'breadcrumbsIntegration', + TryCatch: 'browserApiErrorsIntegration', + GlobalHandlers: 'globalHandlersIntegration', + HttpContext: 'httpContextIntegration', + // Core + InboundFilters: 'inboundFiltersIntegration', + FunctionToString: 'functionToStringIntegration', + LinkedErrors: 'linkedErrorsIntegration', + ModuleMetadata: 'moduleMetadataIntegration', + RequestData: 'requestDataIntegration', + // Integrations + CaptureConsole: 'captureConsoleIntegration', + Debug: 'debugIntegration', + Dedupe: 'dedupeIntegration', + ExtraErrorData: 'extraErrorDataIntegration', + ReportingObserver: 'reportingObserverIntegration', + RewriteFrames: 'rewriteFramesIntegration', + SessionTiming: 'sessionTimingIntegration', + ContextLines: 'contextLinesIntegration', + HttpClient: 'httpClientIntegration', + // Node + Console: 'consoleIntegration', + Http: 'httpIntegration', + OnUncaughtException: 'onUncaughtExceptionIntegration', + OnUnhandledRejection: 'onUnhandledRejectionIntegration', + Modules: 'modulesIntegration', + Context: 'nodeContextIntegration', + LocalVariables: 'localVariablesIntegration', + Undici: 'nodeFetchIntegration', + Spotlight: 'spotlightIntegration', + Anr: 'anrIntegration', + Hapi: 'hapiIntegration', +}; + +const integrationFunctions = Object.values(integrationMap); + +/** + * This transform converts usages of e.g. `new BrowserTracing()` to `browserTracingIntegration()`. + * + * This replaces the integration classes in the following scenarios: + * - `new BrowserTracing()` + * - `new Sentry.BrowserTracing()` + * - `new Integrations.BrowserTracing()` + * - `new Sentry.Integrations.BrowserTracing()` + * + * + * @param {import('jscodeshift').FileInfo} fileInfo + * @param {import('jscodeshift').API} api + * @param {import('jscodeshift').Options & { sentry: import('types').RunOptions & {sdk: string} }} options + */ +module.exports = function (fileInfo, api, options) { + const j = api.jscodeshift; + const source = fileInfo.source; + const fileName = fileInfo.path; + + return wrapJscodeshift(j, source, fileName, (j, source) => { + const tree = j(source); + + // If no sentry import, skip it + if (!hasSentryImportOrRequire(source)) { + return undefined; + } + + const sdk = options.sentry?.sdk; + + // Replace `new Sentry.Integrations.xxx` with `Sentry.xxx` + tree + .find(j.NewExpression, { + callee: { + type: 'MemberExpression', + object: { type: 'MemberExpression', object: { type: 'Identifier' }, property: { type: 'Identifier' } }, + property: { type: 'Identifier' }, + }, + }) + .forEach(path => { + if ( + path.value.callee.type !== 'MemberExpression' || + path.value.callee.object.type !== 'MemberExpression' || + path.value.callee.object.object.type !== 'Identifier' || + path.value.callee.object.property.type !== 'Identifier' || + path.value.callee.property.type !== 'Identifier' + ) { + return; + } + + const objName = path.value.callee.object.object.name; + const maybeIntegrations = path.value.callee.object.property.name; + const className = path.value.callee.property.name; + const args = path.value.arguments; + const fnName = integrationMap[className]; + + if (maybeIntegrations !== INTEGRATIONS_HASH_KEY) { + return; + } + + if (fnName) { + path.replace(j.callExpression(j.memberExpression(j.identifier(objName), j.identifier(fnName)), args)); + return; + } + }); + + // Replace `new BrowserTracing()` + tree.find(j.NewExpression, { callee: { type: 'Identifier' } }).forEach(path => { + if (path.value.callee.type !== 'Identifier') { + return; + } + + const className = path.value.callee.name; + const args = path.value.arguments; + const fnName = integrationMap[className]; + + if (fnName) { + path.replace(j.callExpression(j.identifier(fnName), args)); + return; + } + }); + + // Replace `new Sentry.BrowserTracing()` + tree + .find(j.NewExpression, { + callee: { type: 'MemberExpression', object: { type: 'Identifier' }, property: { type: 'Identifier' } }, + }) + .forEach(path => { + if ( + path.value.callee.type !== 'MemberExpression' || + path.value.callee.object.type !== 'Identifier' || + path.value.callee.property.type !== 'Identifier' + ) { + return; + } + + const objName = path.value.callee.object.name; + const className = path.value.callee.property.name; + const args = path.value.arguments; + const fnName = integrationMap[className]; + + if (fnName) { + path.replace(j.callExpression(j.memberExpression(j.identifier(objName), j.identifier(fnName)), args)); + return; + } + }); + + if (sdk) { + // Replace imports of classes with functions + replaceImported(j, tree, source, sdk, new Map(Object.entries(integrationMap))); + replaceImported(j, tree, source, INTEGRATIONS_PACKAGE, new Map(Object.entries(integrationMap))); + + // Replace `Integrations.browserTracingIntegration()` + // First we check that we actually have a `Integrations` import from our package + let integrationsVarName = undefined; + const integrationsUsed = new Set(); + + const integrationsImport = tree + .find(j.ImportDeclaration, { + source: { type: 'StringLiteral', value: sdk }, + }) + .filter(path => { + const specifiers = path.value.specifiers || []; + + return specifiers.some( + specifier => + specifier.type === 'ImportSpecifier' && + specifier.local?.type === 'Identifier' && + specifier.imported?.name === INTEGRATIONS_HASH_KEY + ); + }); + + integrationsImport.forEach(path => { + // We cache the used import name so we can use it later + const specifiers = path.value.specifiers || []; + + const name = specifiers.find( + specifier => + specifier.type === 'ImportSpecifier' && + specifier.local?.type === 'Identifier' && + specifier.imported?.name === INTEGRATIONS_HASH_KEY + )?.local?.name; + + if (name) { + integrationsVarName = name; + } + }); + + const integrationsRequire = tree.find(j.VariableDeclaration).filter(path => { + return path.value.declarations.some(declaration => { + return ( + declaration.type === 'VariableDeclarator' && + declaration.init?.type === 'CallExpression' && + declaration.init.callee.type === 'Identifier' && + declaration.init.callee.name === 'require' && + declaration.init.arguments.length === 1 && + declaration.init.arguments[0].type === 'StringLiteral' && + declaration.init.arguments[0].value === sdk + ); + }); + }); + + integrationsRequire.forEach(path => { + path.value.declarations.forEach(declaration => { + if (declaration.type === 'VariableDeclarator' && declaration.id.type === 'ObjectPattern') { + const properties = declaration.id.properties || []; + + properties.forEach(property => { + if ( + property.type === 'ObjectProperty' && + property.key.type === 'Identifier' && + property.key.name === INTEGRATIONS_HASH_KEY && + property.value.type === 'Identifier' + ) { + const name = property.value.name; + if (name) { + integrationsVarName = name; + } + } + }); + } + }); + }); + + // This replaces usages of `Integrations.xxx()` with `xxx()` + if (integrationsVarName) { + tree + .find(j.CallExpression, { + callee: { + type: 'MemberExpression', + object: { type: 'Identifier', name: integrationsVarName }, + property: { type: 'Identifier' }, + }, + }) + .forEach(path => { + if ( + path.value.callee.type !== 'MemberExpression' || + path.value.callee.object.type !== 'Identifier' || + path.value.callee.property.type !== 'Identifier' + ) { + return; + } + + const fnName = path.value.callee.property.name; + + // Only handle known integration functions + if (!integrationFunctions.includes(fnName)) { + return; + } + + integrationsUsed.add(fnName); + + path.replace(j.callExpression(j.identifier(fnName), path.value.arguments)); + }); + } + + // Fix imports of `Integrations` + integrationsImport.forEach(path => { + let specifiers = path.value.specifiers || []; + + // Remove the `Integrations` import itself + specifiers = specifiers.filter( + specifier => specifier.type === 'ImportSpecifier' && specifier.imported?.name !== INTEGRATIONS_HASH_KEY + ); + + // Add the new imports (if they don't yet exist) + integrationsUsed.forEach(fnName => { + if (!specifiers.some(specifier => specifier.type === 'ImportSpecifier' && specifier.local?.name === fnName)) { + specifiers.push(j.importSpecifier(j.identifier(fnName))); + } + }); + + // Update import statement + path.value.specifiers = specifiers; + }); + + dedupeImportStatements(sdk, tree, j); + + // Fix requires of `Integrations` + integrationsRequire.forEach(path => { + path.value.declarations.forEach(declaration => { + if (declaration.type === 'VariableDeclarator' && declaration.id.type === 'ObjectPattern') { + let properties = declaration.id.properties || []; + + // Remove the `Integrations` require itself + properties = properties.filter( + prop => + prop.type === 'ObjectProperty' && + prop.key.type === 'Identifier' && + prop.key.name !== INTEGRATIONS_HASH_KEY + ); + + // Add the new requires (if they don't yet exist) + integrationsUsed.forEach(fnName => { + if ( + !properties.some( + prop => prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === fnName + ) + ) { + const prop = j.objectProperty(j.identifier(fnName), j.identifier(fnName)); + prop.shorthand = true; + properties.push(prop); + } + }); + + // Update require statement + declaration.id.properties = properties; + } + }); + }); + } + + return tree.toSource(); + }); +}; diff --git a/src/transformers/rewriteReplayImports/transform.cjs b/src/transformers/rewriteReplayImports/transform.cjs index 1594630..b00910f 100644 --- a/src/transformers/rewriteReplayImports/transform.cjs +++ b/src/transformers/rewriteReplayImports/transform.cjs @@ -1,4 +1,9 @@ -const { rewriteEsmImports, dedupeImportStatements, hasSentryImportOrRequire } = require('../../utils/jscodeshift.cjs'); +const { + rewriteEsmImports, + dedupeImportStatements, + hasSentryImportOrRequire, + rewriteCjsRequires, +} = require('../../utils/jscodeshift.cjs'); const { wrapJscodeshift } = require('../../utils/dom.cjs'); const SENTRY_REPLAY_PACKAGE = '@sentry/replay'; @@ -27,7 +32,7 @@ module.exports = function transform(fileInfo, api, options) { return wrapJscodeshift(j, source, fileName, (j, source) => { const root = j(source, options); - // 1. Replace tracing import with SDK import + // 1. Replace replay import with SDK import const tracingImportPaths = rewriteEsmImports(SENTRY_REPLAY_PACKAGE, options.sentry.sdk, root, j); // 2. Dedupe imports @@ -35,6 +40,9 @@ module.exports = function transform(fileInfo, api, options) { dedupeImportStatements(options.sentry.sdk, root, j); } + // 3. Replace requires + rewriteCjsRequires(SENTRY_REPLAY_PACKAGE, options.sentry.sdk, root, j); + return root.toSource(); }); }; diff --git a/test-fixtures/integrations/dedupeImports.js b/test-fixtures/integrations/dedupeImports.js new file mode 100644 index 0000000..4a78457 --- /dev/null +++ b/test-fixtures/integrations/dedupeImports.js @@ -0,0 +1,8 @@ +import { BrowserTracing, Integrations, breadcrumbsIntegration } from '@sentry/browser'; + +function doSomething() { + const a = new BrowserTracing(); + const b = new Integrations.BrowserTracing(); + const c = breadcrumbsIntegration(); + const d = new Integrations.Breadcrumbs(); +} diff --git a/test-fixtures/integrations/dedupeRequire.js b/test-fixtures/integrations/dedupeRequire.js new file mode 100644 index 0000000..1986f81 --- /dev/null +++ b/test-fixtures/integrations/dedupeRequire.js @@ -0,0 +1,8 @@ +const { BrowserTracing, Integrations, breadcrumbsIntegration } = require('@sentry/browser'); + +function doSomething() { + const a = new BrowserTracing(); + const b = new Integrations.BrowserTracing(); + const c = breadcrumbsIntegration(); + const d = new Integrations.Breadcrumbs(); +} diff --git a/test-fixtures/integrations/integrationsFunctionalImport.js b/test-fixtures/integrations/integrationsFunctionalImport.js new file mode 100644 index 0000000..3e5ce34 --- /dev/null +++ b/test-fixtures/integrations/integrationsFunctionalImport.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; +import { httpClientIntegration } from '@sentry/integrations'; + +function doSomething() { + Sentry.init({ + integrations: [httpClientIntegration()] + }); +} diff --git a/test-fixtures/integrations/simpleImport.js b/test-fixtures/integrations/simpleImport.js new file mode 100644 index 0000000..038da21 --- /dev/null +++ b/test-fixtures/integrations/simpleImport.js @@ -0,0 +1,7 @@ +import { init, BrowserTracing } from '@sentry/browser'; + +function doSomething() { + init({ + integrations: [new BrowserTracing()] + }); +} diff --git a/test-fixtures/integrations/simpleImportIntegrations.js b/test-fixtures/integrations/simpleImportIntegrations.js new file mode 100644 index 0000000..b7c81e1 --- /dev/null +++ b/test-fixtures/integrations/simpleImportIntegrations.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; +import { HttpClient } from '@sentry/integrations'; + +function doSomething() { + Sentry.init({ + integrations: [new HttpClient()] + }); +} diff --git a/test-fixtures/integrations/simpleNamespaceImport.js b/test-fixtures/integrations/simpleNamespaceImport.js new file mode 100644 index 0000000..e16456c --- /dev/null +++ b/test-fixtures/integrations/simpleNamespaceImport.js @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/browser'; + +function doSomething() { + Sentry.init({ + integrations: [new Sentry.BrowserTracing()] + }); +} diff --git a/test-fixtures/integrations/simpleNamespaceRequire.js b/test-fixtures/integrations/simpleNamespaceRequire.js new file mode 100644 index 0000000..afc41ab --- /dev/null +++ b/test-fixtures/integrations/simpleNamespaceRequire.js @@ -0,0 +1,7 @@ +const Sentry = require('@sentry/browser'); + +function doSomething() { + Sentry.init({ + integrations: [new Sentry.BrowserTracing()] + }); +} diff --git a/test-fixtures/integrations/simpleRequire.js b/test-fixtures/integrations/simpleRequire.js new file mode 100644 index 0000000..21bd4e5 --- /dev/null +++ b/test-fixtures/integrations/simpleRequire.js @@ -0,0 +1,7 @@ +const { init, BrowserTracing } = require('@sentry/browser'); + +function doSomething() { + init({ + integrations: [new BrowserTracing()] + }); +} diff --git a/test-fixtures/integrations/simpleRequireIntegrations.js b/test-fixtures/integrations/simpleRequireIntegrations.js new file mode 100644 index 0000000..b411563 --- /dev/null +++ b/test-fixtures/integrations/simpleRequireIntegrations.js @@ -0,0 +1,8 @@ +const Sentry = require('@sentry/browser'); +const { HttpClient } = require('@sentry/integrations'); + +function doSomething() { + Sentry.init({ + integrations: [new HttpClient()] + }); +} diff --git a/test-fixtures/integrations/withImports.js b/test-fixtures/integrations/withImports.js new file mode 100644 index 0000000..8ed447d --- /dev/null +++ b/test-fixtures/integrations/withImports.js @@ -0,0 +1,66 @@ +import { BrowserTracing, Integrations } from '@sentry/browser'; +import * as Sentry from '@sentry/browser'; +import * as SentryIntegrations from '@sentry/integrations'; +import { HttpClient } from '@sentry/integrations'; + +function orig() { + // do something +} + +function doSomething() { + // Check different invocations + const a = new BrowserTracing(); + const b = new BrowserTracing({ option: 'value' }); + const c = new Sentry.BrowserTracing({ option: 'value' }); + const d = new Integrations.BrowserTracing({ option: 'value' }); + const e = new Integrations.Breadcrumbs({ option: 'value' }); + const f = new Integrations.CaptureConsole({ option: 'value' }); + const g = new Sentry.Integrations.ContextLines(); + const h = new Sentry.SomethingElse.Span(); + + const integrations = [ + // Browser + new Sentry.BrowserTracing(), + new Sentry.Replay(), + new Sentry.Feedback(), + new Sentry.Breadcrumbs(), + new Sentry.TryCatch(), + new Sentry.GlobalHandlers(), + new Sentry.HttpContext(), + // Core + new Sentry.InboundFilters(), + new Sentry.FunctionToString(), + new Sentry.LinkedErrors(), + new Sentry.ModuleMetadata(), + new Sentry.RequestData(), + // Integrations + new SentryIntegrations.HttpClient(), + new HttpClient(), + new SentryIntegrations.CaptureConsole(), + new SentryIntegrations.Debug(), + new SentryIntegrations.Dedupe(), + new SentryIntegrations.ExtraErrorData(), + new SentryIntegrations.ReportingObserver(), + new SentryIntegrations.RewriteFrames(), + new SentryIntegrations.SessionTiming(), + new SentryIntegrations.ContextLines(), + // Node + new Sentry.Console(), + new Sentry.Http(), + new Sentry.OnUncaughtException(), + new Sentry.OnUnhandledRejection(), + new Sentry.Modules(), + new Sentry.ContextLines(), + new Sentry.Context(), + new Sentry.LocalVariables(), + new Sentry.Undici(), + new Sentry.Spotlight(), + new Sentry.Anr(), + new Sentry.Hapi(), + ]; + + // Other classes are ignored + const x = new MyClass(); + const y = new Sentry.Span(); + const z = new SentryIntegrations.MyIntegration(); +} diff --git a/test-fixtures/integrations/withImports.ts b/test-fixtures/integrations/withImports.ts new file mode 100644 index 0000000..5c3a8c1 --- /dev/null +++ b/test-fixtures/integrations/withImports.ts @@ -0,0 +1,66 @@ +import { BrowserTracing, Integrations } from '@sentry/browser'; +import * as Sentry from '@sentry/browser'; +import * as SentryIntegrations from '@sentry/integrations'; +import { HttpClient } from '@sentry/integrations'; + +function orig(): void { + // do something +} + +function doSomething(): void { + // Check different invocations + const a = new BrowserTracing(); + const b = new BrowserTracing({ option: 'value' }); + const c = new Sentry.BrowserTracing({ option: 'value' }); + const d = new Integrations.BrowserTracing({ option: 'value' }); + const e = new Integrations.Breadcrumbs({ option: 'value' }); + const f = new Integrations.CaptureConsole({ option: 'value' }); + const g = new Sentry.Integrations.ContextLines(); + const h = new Sentry.SomethingElse.Span(); + + const integrations = [ + // Browser + new Sentry.BrowserTracing(), + new Sentry.Replay(), + new Sentry.Feedback(), + new Sentry.Breadcrumbs(), + new Sentry.TryCatch(), + new Sentry.GlobalHandlers(), + new Sentry.HttpContext(), + // Core + new Sentry.InboundFilters(), + new Sentry.FunctionToString(), + new Sentry.LinkedErrors(), + new Sentry.ModuleMetadata(), + new Sentry.RequestData(), + // Integrations + new SentryIntegrations.HttpClient(), + new HttpClient(), + new SentryIntegrations.CaptureConsole(), + new SentryIntegrations.Debug(), + new SentryIntegrations.Dedupe(), + new SentryIntegrations.ExtraErrorData(), + new SentryIntegrations.ReportingObserver(), + new SentryIntegrations.RewriteFrames(), + new SentryIntegrations.SessionTiming(), + new SentryIntegrations.ContextLines(), + // Node + new Sentry.Console(), + new Sentry.Http(), + new Sentry.OnUncaughtException(), + new Sentry.OnUnhandledRejection(), + new Sentry.Modules(), + new Sentry.ContextLines(), + new Sentry.Context(), + new Sentry.LocalVariables(), + new Sentry.Undici(), + new Sentry.Spotlight(), + new Sentry.Anr(), + new Sentry.Hapi(), + ]; + + // Other classes are ignored + const x = new MyClass(); + const y = new Sentry.Span(); + const z = new SentryIntegrations.MyIntegration(); +} diff --git a/test-fixtures/integrations/withRequire.js b/test-fixtures/integrations/withRequire.js new file mode 100644 index 0000000..68f6fa0 --- /dev/null +++ b/test-fixtures/integrations/withRequire.js @@ -0,0 +1,66 @@ +const { BrowserTracing, Integrations } = require('@sentry/browser'); +const Sentry = require('@sentry/browser'); +const SentryIntegrations = require('@sentry/integrations'); +const { HttpClient } = require('@sentry/integrations'); + +function orig() { + // do something +} + +function doSomething() { + // Check different invocations + const a = new BrowserTracing(); + const b = new BrowserTracing({ option: 'value' }); + const c = new Sentry.BrowserTracing({ option: 'value' }); + const d = new Integrations.BrowserTracing({ option: 'value' }); + const e = new Integrations.Breadcrumbs({ option: 'value' }); + const f = new Integrations.CaptureConsole({ option: 'value' }); + const g = new Sentry.Integrations.ContextLines(); + const h = new Sentry.SomethingElse.Span(); + + const integrations = [ + // Browser + new Sentry.BrowserTracing(), + new Sentry.Replay(), + new Sentry.Feedback(), + new Sentry.Breadcrumbs(), + new Sentry.TryCatch(), + new Sentry.GlobalHandlers(), + new Sentry.HttpContext(), + // Core + new Sentry.InboundFilters(), + new Sentry.FunctionToString(), + new Sentry.LinkedErrors(), + new Sentry.ModuleMetadata(), + new Sentry.RequestData(), + // Integrations + new SentryIntegrations.HttpClient(), + new HttpClient(), + new SentryIntegrations.CaptureConsole(), + new SentryIntegrations.Debug(), + new SentryIntegrations.Dedupe(), + new SentryIntegrations.ExtraErrorData(), + new SentryIntegrations.ReportingObserver(), + new SentryIntegrations.RewriteFrames(), + new SentryIntegrations.SessionTiming(), + new SentryIntegrations.ContextLines(), + // Node + new Sentry.Console(), + new Sentry.Http(), + new Sentry.OnUncaughtException(), + new Sentry.OnUnhandledRejection(), + new Sentry.Modules(), + new Sentry.ContextLines(), + new Sentry.Context(), + new Sentry.LocalVariables(), + new Sentry.Undici(), + new Sentry.Spotlight(), + new Sentry.Anr(), + new Sentry.Hapi(), + ]; + + // Other classes are ignored + const x = new MyClass(); + const y = new Sentry.Span(); + const z = new SentryIntegrations.MyIntegration(); +}