From a2b36494b8a06dd9999133f7068e609eb47e58ba Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 22 Jan 2025 12:41:29 +0000 Subject: [PATCH 1/3] feat(angular): Add Angular menu option and install SDK (#765) Adds Angular menu option with SDK installation --------- Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- lib/Constants.ts | 3 + src/angular/angular-wizard.ts | 114 ++++++++++++++++++++++++++++++++++ src/run.ts | 7 +++ src/utils/clack-utils.ts | 2 + 4 files changed, 126 insertions(+) create mode 100644 src/angular/angular-wizard.ts diff --git a/lib/Constants.ts b/lib/Constants.ts index 4350d9a0..09eb6a4b 100644 --- a/lib/Constants.ts +++ b/lib/Constants.ts @@ -4,6 +4,7 @@ export enum Integration { ios = 'ios', android = 'android', cordova = 'cordova', + angular = 'angular', electron = 'electron', nextjs = 'nextjs', nuxt = 'nuxt', @@ -68,6 +69,8 @@ export function mapIntegrationToPlatform(type: string): string | undefined { return 'react-native'; case Integration.cordova: return 'cordova'; + case Integration.angular: + return 'javascript-angular'; case Integration.electron: return 'javascript-electron'; case Integration.nextjs: diff --git a/src/angular/angular-wizard.ts b/src/angular/angular-wizard.ts new file mode 100644 index 00000000..72518431 --- /dev/null +++ b/src/angular/angular-wizard.ts @@ -0,0 +1,114 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import clack from '@clack/prompts'; + +import chalk from 'chalk'; +import type { WizardOptions } from '../utils/types'; +import { withTelemetry } from '../telemetry'; +import { + abortIfCancelled, + confirmContinueIfNoOrDirtyGitRepo, + ensurePackageIsInstalled, + getPackageDotJson, + installPackage, + printWelcome, +} from '../utils/clack-utils'; +import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; +import { gte, minVersion, SemVer } from 'semver'; + +import * as Sentry from '@sentry/node'; + +const MIN_SUPPORTED_ANGULAR_VERSION = '14.0.0'; + +export async function runAngularWizard(options: WizardOptions): Promise { + return withTelemetry( + { + enabled: options.telemetryEnabled, + integration: 'angular', + wizardOptions: options, + }, + () => runAngularWizardWithTelemetry(options), + ); +} + +async function runAngularWizardWithTelemetry( + options: WizardOptions, +): Promise { + printWelcome({ + wizardName: 'Sentry Angular Wizard', + promoCode: options.promoCode, + telemetryEnabled: options.telemetryEnabled, + }); + + await confirmContinueIfNoOrDirtyGitRepo(); + + const packageJson = await getPackageDotJson(); + + await ensurePackageIsInstalled(packageJson, '@angular/core', 'Angular'); + + let installedAngularVersion = getPackageVersion('@angular/core', packageJson); + + if (!installedAngularVersion) { + clack.log.warn('Could not determine installed Angular version.'); + + installedAngularVersion = await abortIfCancelled( + clack.text({ + message: `Please enter your installed Angular major version (e.g. ${chalk.cyan( + '18', + )} for Angular 18)`, + validate(value) { + if (!value) { + return 'Angular version is required'; + } + + try { + if (!minVersion(value)) { + return `Invalid Angular version provided: ${value}`; + } + } catch (error) { + return `Invalid Angular version provided: ${value}`; + } + }, + }), + ); + } + + Sentry.setTag('angular-version', installedAngularVersion); + + const installedMinVersion = minVersion(installedAngularVersion) as SemVer; + + const isSupportedAngularVersion = gte( + installedMinVersion, + MIN_SUPPORTED_ANGULAR_VERSION, + ); + + if (!isSupportedAngularVersion) { + clack.log.warn( + `Angular version ${chalk.cyan( + MIN_SUPPORTED_ANGULAR_VERSION, + )} or higher is required.`, + ); + clack.log.warn( + `Please refer to Sentry's version compatibility table for more information: +${chalk.underline( + 'https://docs.sentry.io/platforms/javascript/guides/angular/#angular-version-compatibility', +)}`, + ); + + return; + } + + const sdkAlreadyInstalled = hasPackageInstalled( + '@sentry/angular', + packageJson, + ); + + Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); + + await installPackage({ + packageName: '@sentry/angular@^8', + packageNameDisplayLabel: '@sentry/angular', + alreadyInstalled: sdkAlreadyInstalled, + }); +} diff --git a/src/run.ts b/src/run.ts index a0dc6192..0eaae784 100644 --- a/src/run.ts +++ b/src/run.ts @@ -6,6 +6,7 @@ import { runReactNativeWizard } from './react-native/react-native-wizard'; import { run as legacyRun } from '../lib/Setup'; import type { PreselectedProject, WizardOptions } from './utils/types'; import { runAndroidWizard } from './android/android-wizard'; +import { runAngularWizard } from './angular/angular-wizard'; import { runAppleWizard } from './apple/apple-wizard'; import { runNextjsWizard } from './nextjs/nextjs-wizard'; import { runNuxtWizard } from './nuxt/nuxt-wizard'; @@ -17,6 +18,7 @@ import type { Platform } from '../lib/Constants'; import type { PackageDotJson } from './utils/package-json'; type WizardIntegration = + | 'angular' | 'reactNative' | 'ios' | 'android' @@ -101,6 +103,7 @@ export async function run(argv: Args) { options: [ { value: 'reactNative', label: 'React Native' }, { value: 'ios', label: 'iOS' }, + { value: 'angular', label: 'Angular' }, { value: 'android', label: 'Android' }, { value: 'cordova', label: 'Cordova' }, { value: 'electron', label: 'Electron' }, @@ -147,6 +150,10 @@ export async function run(argv: Args) { await runAndroidWizard(wizardOptions); break; + case 'angular': + await runAngularWizard(wizardOptions); + break; + case 'nextjs': await runNextjsWizard(wizardOptions); break; diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 1d22bb10..6db620a7 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -875,6 +875,7 @@ export function isUsingTypeScript() { export async function getOrAskForProjectData( options: WizardOptions, platform?: + | 'javascript-angular' | 'javascript-nextjs' | 'javascript-nuxt' | 'javascript-remix' @@ -1037,6 +1038,7 @@ async function askForWizardLogin(options: { url: string; promoCode?: string; platform?: + | 'javascript-angular' | 'javascript-nextjs' | 'javascript-nuxt' | 'javascript-remix' From e49c282b0037872c50a704a9491cfd50cbc0b52b Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 22 Jan 2025 14:06:48 +0000 Subject: [PATCH 2/3] Add CHANGELOG entry. --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c233a8..e3562ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- feat: Add Angular Wizard ([#767](https://github.com/getsentry/sentry-wizard/pull/767)) + ## 3.39.0 - Always send platform query param to auth page ([#757](https://github.com/getsentry/sentry-wizard/pull/757)) @@ -28,7 +32,7 @@ - feat: Pin JS SDK versions to v8 (#712) - Remove enableTracing for Cocoa ([#715](https://github.com/getsentry/sentry-wizard/pull/715)) - feat(nuxt): Add nuxt wizard ([#719](https://github.com/getsentry/sentry-wizard/pull/719)) - + Set up the Sentry Nuxt SDK in your app with one command: ```sh From 49dda8362753e6db0b22e0229e0f0cdf9552303b Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 3 Feb 2025 16:09:53 +0000 Subject: [PATCH 3/3] feat(angular): Add Sentry setup in `main.ts` (#768) - Adds Sentry initialization on main.ts with feature selection - Also adds Prettier run --- src/angular/angular-wizard.ts | 46 +++++++++++++++- src/angular/codemods/main.ts | 100 ++++++++++++++++++++++++++++++++++ src/angular/sdk-setup.ts | 72 ++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/angular/codemods/main.ts create mode 100644 src/angular/sdk-setup.ts diff --git a/src/angular/angular-wizard.ts b/src/angular/angular-wizard.ts index 72518431..ad4da7ae 100644 --- a/src/angular/angular-wizard.ts +++ b/src/angular/angular-wizard.ts @@ -5,19 +5,23 @@ import clack from '@clack/prompts'; import chalk from 'chalk'; import type { WizardOptions } from '../utils/types'; -import { withTelemetry } from '../telemetry'; +import { traceStep, withTelemetry } from '../telemetry'; import { abortIfCancelled, confirmContinueIfNoOrDirtyGitRepo, ensurePackageIsInstalled, + featureSelectionPrompt, + getOrAskForProjectData, getPackageDotJson, installPackage, printWelcome, + runPrettierIfInstalled, } from '../utils/clack-utils'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; import { gte, minVersion, SemVer } from 'semver'; import * as Sentry from '@sentry/node'; +import { initalizeSentryOnApplicationEntry } from './sdk-setup'; const MIN_SUPPORTED_ANGULAR_VERSION = '14.0.0'; @@ -99,6 +103,11 @@ ${chalk.underline( return; } + const { selectedProject } = await getOrAskForProjectData( + options, + 'javascript-angular', + ); + const sdkAlreadyInstalled = hasPackageInstalled( '@sentry/angular', packageJson, @@ -111,4 +120,39 @@ ${chalk.underline( packageNameDisplayLabel: '@sentry/angular', alreadyInstalled: sdkAlreadyInstalled, }); + + const dsn = selectedProject.keys[0].dsn.public; + + const selectedFeatures = await featureSelectionPrompt([ + { + id: 'performance', + prompt: `Do you want to enable ${chalk.bold( + 'Tracing', + )} to track the performance of your application?`, + enabledHint: 'recommended', + }, + { + id: 'replay', + prompt: `Do you want to enable ${chalk.bold( + 'Sentry Session Replay', + )} to get a video-like reproduction of errors during a user session?`, + enabledHint: 'recommended, but increases bundle size', + }, + ] as const); + + await traceStep( + 'Initialize Sentry on Angular application entry point', + async () => { + await initalizeSentryOnApplicationEntry(dsn, selectedFeatures); + }, + ); + + await traceStep('Run Prettier', async () => { + await runPrettierIfInstalled(); + }); + + clack.outro(` + ${chalk.green( + 'Sentry has been successfully configured for your Angular project.', + )}`); } diff --git a/src/angular/codemods/main.ts b/src/angular/codemods/main.ts new file mode 100644 index 00000000..24b984b5 --- /dev/null +++ b/src/angular/codemods/main.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import type { Program } from '@babel/types'; + +// @ts-expect-error - magicast is ESM and TS complains about that. It works though +import { builders, generateCode, type ProxifiedModule } from 'magicast'; + +export function updateAppEntryMod( + originalAppModuleMod: ProxifiedModule, + dsn: string, + selectedFeatures: { + performance: boolean; + replay: boolean; + }, +): ProxifiedModule { + originalAppModuleMod.imports.$add({ + from: '@sentry/angular', + imported: '*', + local: 'Sentry', + }); + + insertInitCall(originalAppModuleMod, dsn, selectedFeatures); + + return originalAppModuleMod; +} + +export function insertInitCall( + originalAppModuleMod: ProxifiedModule, + dsn: string, + selectedFeatures: { + performance: boolean; + replay: boolean; + }, +): void { + const initCallArgs = getInitCallArgs(dsn, selectedFeatures); + const initCall = builders.functionCall('Sentry.init', initCallArgs); + const originalAppModuleModAst = originalAppModuleMod.$ast as Program; + + const initCallInsertionIndex = getAfterImportsInsertionIndex( + originalAppModuleModAst, + ); + + originalAppModuleModAst.body.splice( + initCallInsertionIndex, + 0, + // @ts-expect-error - string works here because the AST is proxified by magicast + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + generateCode(initCall).code, + ); +} + +export function getInitCallArgs( + dsn: string, + selectedFeatures: { + performance: boolean; + replay: boolean; + }, +): Record { + const initCallArgs = { + dsn, + } as Record; + + if (selectedFeatures.replay || selectedFeatures.performance) { + initCallArgs.integrations = []; + + if (selectedFeatures.performance) { + // @ts-expect-error - Adding Proxified AST node to the array + initCallArgs.integrations.push( + builders.functionCall('Sentry.browserTracingIntegration'), + ); + initCallArgs.tracesSampleRate = 1.0; + } + + if (selectedFeatures.replay) { + // @ts-expect-error - Adding Proxified AST node to the array + initCallArgs.integrations.push( + builders.functionCall('Sentry.replayIntegration'), + ); + + initCallArgs.replaysSessionSampleRate = 0.1; + initCallArgs.replaysOnErrorSampleRate = 1.0; + } + } + + return initCallArgs; +} + +/** + * We want to insert the handleError function just after all imports + */ +export function getAfterImportsInsertionIndex( + originalEntryServerModAST: Program, +): number { + for (let x = originalEntryServerModAST.body.length - 1; x >= 0; x--) { + if (originalEntryServerModAST.body[x].type === 'ImportDeclaration') { + return x + 1; + } + } + + return 0; +} diff --git a/src/angular/sdk-setup.ts b/src/angular/sdk-setup.ts new file mode 100644 index 00000000..96eb8a5f --- /dev/null +++ b/src/angular/sdk-setup.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +// @ts-expect-error - magicast is ESM and TS complains about that. It works though +import { loadFile, writeFile } from 'magicast'; + +import * as path from 'path'; + +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import clack from '@clack/prompts'; +import chalk from 'chalk'; +import { updateAppEntryMod } from './codemods/main'; +import { hasSentryContent } from '../utils/ast-utils'; +import type { namedTypes as t } from 'ast-types'; + +export async function initalizeSentryOnApplicationEntry( + dsn: string, + selectedFeatures: { + performance: boolean; + replay: boolean; + }, +): Promise { + const appEntryFilename = 'main.ts'; + const appEntryPath = path.join(process.cwd(), 'src', appEntryFilename); + + const originalAppEntry = await loadFile(appEntryPath); + + if (hasSentryContent(originalAppEntry.$ast as t.Program)) { + clack.log.warn( + `File ${chalk.cyan(appEntryFilename)} already contains Sentry. +Skipping adding Sentry functionality to ${chalk.cyan(appEntryFilename)}.`, + ); + + return; + } + + try { + const updatedAppEntryMod = updateAppEntryMod( + originalAppEntry, + dsn, + selectedFeatures, + ); + + await writeFile(updatedAppEntryMod.$ast, appEntryPath); + } catch (error: unknown) { + clack.log.error( + `Error while adding Sentry to ${chalk.cyan(appEntryFilename)}`, + ); + + clack.log.info( + chalk.dim( + typeof error === 'object' && error != null && 'toString' in error + ? error.toString() + : typeof error === 'string' + ? error + : '', + ), + ); + + clack.log.warn( + `Please refer to the documentation for manual setup: +${chalk.underline( + 'https://docs.sentry.io/platforms/javascript/guides/angular/#configure', +)}`, + ); + + return; + } + + clack.log.success( + `Successfully initialized Sentry on ${chalk.cyan(appEntryFilename)}`, + ); +}