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)}`, + ); +}