Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ module.exports = {
'checkIfRunsOnDevMode',
'checkEnvBuildPlugin',
'checkSentryCliRc',
'assertTransform',
'assertNoChange',
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/tests/help-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('--help command', () => {
env: SENTRY_WIZARD_INTEGRATION
[choices: "reactNative", "flutter", "ios", "android", "cordova", "angular",
"cloudflare", "electron", "nextjs", "nuxt", "remix", "reactRouter",
"sveltekit", "sourcemaps"]
"sveltekit", "sourcemaps", "upgrade"]
-p, --platform Choose platform(s)
env: SENTRY_WIZARD_PLATFORM
[array] [choices: "ios", "android"]
Expand Down
1 change: 1 addition & 0 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum Integration {
reactRouter = 'reactRouter',
sveltekit = 'sveltekit',
sourcemaps = 'sourcemaps',
upgrade = 'upgrade',
}

/** Key value should be the same here */
Expand Down
9 changes: 8 additions & 1 deletion src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { runSourcemapsWizard } from './sourcemaps/sourcemaps-wizard';
import { runSvelteKitWizard } from './sveltekit/sveltekit-wizard';
import { runReactRouterWizard } from './react-router/react-router-wizard';
import { runCloudflareWizard } from './cloudflare/cloudflare-wizard';
import { runUpgradeWizard } from './upgrade/upgrade-wizard';
import { enableDebugLogs } from './utils/debug';
import type { PreselectedProject, WizardOptions } from './utils/types';
import { WIZARD_VERSION } from './version';
Expand All @@ -35,7 +36,8 @@ type WizardIntegration =
| 'reactRouter'
| 'sveltekit'
| 'cloudflare'
| 'sourcemaps';
| 'sourcemaps'
| 'upgrade';

type Args = {
integration?: WizardIntegration;
Expand Down Expand Up @@ -132,6 +134,7 @@ export async function run(argv: Args) {
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'cloudflare', label: 'Cloudflare' },
{ value: 'sourcemaps', label: 'Configure Source Maps Upload' },
{ value: 'upgrade', label: 'Upgrade SDK (apply codemods)' },
],
}),
);
Expand Down Expand Up @@ -210,6 +213,10 @@ export async function run(argv: Args) {
await runSourcemapsWizard(wizardOptions);
break;

case 'upgrade':
await runUpgradeWizard({ projectDir: process.cwd() });
break;

case 'cordova':
argv.integration = 'cordova';
void legacyRun(
Expand Down
94 changes: 94 additions & 0 deletions src/upgrade/codemod-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as fs from 'fs';
import * as recast from 'recast';
import type {
CodemodTransform,
CodemodResult,
ManualReviewItem,
} from './types.js';

// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
const babelTsParser = require('recast/parsers/babel-ts');

export interface RunResult {
filesModified: number;
totalChanges: string[];
manualReviewItems: ManualReviewItem[];
errors: { file: string; error: string }[];
}

export function runCodemodsOnFile(
filePath: string,
sourceCode: string,
transforms: CodemodTransform[],
): { output: string; result: CodemodResult } {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
const ast = recast.parse(sourceCode, { parser: babelTsParser });
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const program = ast.program as recast.types.namedTypes.Program;

let anyModified = false;
const allChanges: string[] = [];
const allManualReview: ManualReviewItem[] = [];

for (const transform of transforms) {
const result = transform.transform({
program,
filePath,
sourceCode,
});

if (result.modified) {
anyModified = true;
}
allChanges.push(...result.changes);
allManualReview.push(...result.manualReviewItems);
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const output = recast.print(ast).code;

return {
output,
result: {
modified: anyModified,
changes: allChanges,
manualReviewItems: allManualReview,
},
};
}

export function runCodemodsOnFiles(
files: string[],
transforms: CodemodTransform[],
): RunResult {
let filesModified = 0;
const totalChanges: string[] = [];
const manualReviewItems: ManualReviewItem[] = [];
const errors: { file: string; error: string }[] = [];

for (const file of files) {
try {
const sourceCode = fs.readFileSync(file, 'utf-8');
const { output, result } = runCodemodsOnFile(
file,
sourceCode,
transforms,
);

if (result.modified) {
fs.writeFileSync(file, output, 'utf-8');
filesModified++;
totalChanges.push(...result.changes.map((c) => `${file}: ${c}`));
}

manualReviewItems.push(...result.manualReviewItems);
} catch (e) {
errors.push({
file,
error: e instanceof Error ? e.message : String(e),
});
}
}

return { filesModified, totalChanges, manualReviewItems, errors };
}
149 changes: 149 additions & 0 deletions src/upgrade/codemods/v8-to-v9/config-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as recast from 'recast';
import x = recast.types;
import t = x.namedTypes;
import type {
CodemodTransform,
TransformContext,
CodemodResult,
ManualReviewItem,
} from '../../types.js';

const b = recast.types.builders;

// Config options to remove completely
const REMOVE_OPTIONS = ['hideSourceMaps', 'autoInstrumentRemix'];

// Config options that need manual review
const MANUAL_REVIEW_OPTIONS: Record<string, string> = {
autoSessionTracking:
"'autoSessionTracking' was removed in v9. Session tracking is now always enabled when a release is set. Remove this option and configure release tracking instead.",
};

function getLineNumber(node: t.Node): number {
return node.loc?.start.line ?? 0;
}

export const configChanges: CodemodTransform = {
name: 'config-changes',
description:
'Removes deprecated config options (enableTracing, hideSourceMaps, etc.) and flattens transactionContext',

transform(ctx: TransformContext): CodemodResult {
let modified = false;
const changes: string[] = [];
const manualReviewItems: ManualReviewItem[] = [];

recast.visit(ctx.program, {
visitObjectExpression(path) {
const props = path.node.properties;

// Process in reverse to safely splice
for (let i = props.length - 1; i >= 0; i--) {
const prop = props[i];
if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') {
continue;
}

const key = prop.key;
let propName: string | null = null;
if (key.type === 'Identifier') {
propName = key.name;
} else if (
(key.type === 'StringLiteral' || key.type === 'Literal') &&
typeof key.value === 'string'
) {
propName = key.value;
}

if (!propName) {
continue;
}

// enableTracing: true → tracesSampleRate: 1.0
// enableTracing: false → remove
if (propName === 'enableTracing') {
const value = prop.value;
const isTruthy =
(value.type === 'BooleanLiteral' && value.value === true) ||
(value.type === 'Literal' && value.value === true);

if (isTruthy) {
// Replace with tracesSampleRate: 1.0, with a TODO comment
const newProp = b.objectProperty(
b.identifier('tracesSampleRate'),
b.numericLiteral(1.0),
);
newProp.comments = [
b.commentLine(
" TODO(sentry-upgrade): 'enableTracing' was removed. Use tracesSampleRate instead.",
true,
false,
),
];
props.splice(i, 1, newProp);
} else {
props.splice(i, 1);
}

modified = true;
changes.push("Removed 'enableTracing' option");
continue;
}

// Simple removals
if (REMOVE_OPTIONS.includes(propName)) {
props.splice(i, 1);
modified = true;
changes.push(`Removed '${propName}' option`);
continue;
}

// Manual review options
if (propName in MANUAL_REVIEW_OPTIONS) {
manualReviewItems.push({
file: ctx.filePath,
line: getLineNumber(prop),
description: MANUAL_REVIEW_OPTIONS[propName],
});
// Still remove the property
props.splice(i, 1);
modified = true;
changes.push(`Removed '${propName}' (needs manual review)`);
continue;
}
}

this.traverse(path);
},
});

// Flatten transactionContext access in tracesSampler
recast.visit(ctx.program, {
visitMemberExpression(path) {
const node = path.node;

// Look for: *.transactionContext.property
if (
node.property.type === 'Identifier' &&
node.object.type === 'MemberExpression' &&
node.object.property.type === 'Identifier' &&
node.object.property.name === 'transactionContext'
) {
// Replace X.transactionContext.Y with X.Y
const outerObject = node.object.object;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
path.replace(b.memberExpression(outerObject as any, node.property));
modified = true;
changes.push(
`Flattened transactionContext.${node.property.name} access`,
);
return false;
}

this.traverse(path);
},
});

return { modified, changes, manualReviewItems };
},
};
Loading
Loading