From b8544fa8b601a754d0aab4d9f6dc7805cfae8c14 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 22 Jan 2025 14:32:02 +0000 Subject: [PATCH 1/4] feat(angular): Add Sentry setup in `main.ts` --- src/angular/angular-wizard.ts | 46 +++++++++++++++- src/angular/codemods/main.ts | 100 ++++++++++++++++++++++++++++++++++ src/angular/sdk-setup.ts | 86 +++++++++++++++++++++++++++++ 3 files changed, 231 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..fadfe8b2 --- /dev/null +++ b/src/angular/sdk-setup.ts @@ -0,0 +1,86 @@ +/* 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'; + +export function hasSentryContent( + fileName: string, + fileContent: string, + expectedContent = '@sentry/angular', +): boolean { + const includesContent = fileContent.includes(expectedContent); + + if (includesContent) { + clack.log.warn( + `File ${chalk.cyan( + path.basename(fileName), + )} already contains ${expectedContent}. +Skipping adding Sentry functionality to ${chalk.cyan( + path.basename(fileName), + )}.`, + ); + } + + return includesContent; +} + +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(appEntryPath, originalAppEntry.$code)) { + 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)}`, + ); +} From 590afd7b90f7766c1b221cfbf87e9acbb14c39c0 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 31 Jan 2025 14:01:20 +0000 Subject: [PATCH 2/4] Use `hasSentryContent` from `ast-utils` --- src/angular/sdk-setup.ts | 42 +++++++++++++++------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/angular/sdk-setup.ts b/src/angular/sdk-setup.ts index fadfe8b2..a3e8d239 100644 --- a/src/angular/sdk-setup.ts +++ b/src/angular/sdk-setup.ts @@ -9,27 +9,8 @@ import * as path from 'path'; import clack from '@clack/prompts'; import chalk from 'chalk'; import { updateAppEntryMod } from './codemods/main'; - -export function hasSentryContent( - fileName: string, - fileContent: string, - expectedContent = '@sentry/angular', -): boolean { - const includesContent = fileContent.includes(expectedContent); - - if (includesContent) { - clack.log.warn( - `File ${chalk.cyan( - path.basename(fileName), - )} already contains ${expectedContent}. -Skipping adding Sentry functionality to ${chalk.cyan( - path.basename(fileName), - )}.`, - ); - } - - return includesContent; -} +import { hasSentryContent } from '../utils/ast-utils'; +import type { namedTypes as t } from 'ast-types'; export async function initalizeSentryOnApplicationEntry( dsn: string, @@ -43,7 +24,16 @@ export async function initalizeSentryOnApplicationEntry( const originalAppEntry = await loadFile(appEntryPath); - if (hasSentryContent(appEntryPath, originalAppEntry.$code)) { + if (hasSentryContent(originalAppEntry.$ast as t.Program)) { + clack.log.warn( + `File ${chalk.cyan( + path.basename(appEntryPath), + )} already contains Sentry. +Skipping adding Sentry functionality to ${chalk.cyan( + path.basename(appEntryPath), + )}.`, + ); + return; } @@ -65,16 +55,16 @@ export async function initalizeSentryOnApplicationEntry( typeof error === 'object' && error != null && 'toString' in error ? error.toString() : typeof error === 'string' - ? error - : '', + ? error + : '', ), ); clack.log.warn( `Please refer to the documentation for manual setup: ${chalk.underline( - 'https://docs.sentry.io/platforms/javascript/guides/angular/#configure', -)}`, + 'https://docs.sentry.io/platforms/javascript/guides/angular/#configure', + )}`, ); return; From 74db58ef3a9e605d34daf771ec72ff9a4dd670fd Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 31 Jan 2025 14:58:47 +0000 Subject: [PATCH 3/4] Lint --- src/angular/sdk-setup.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/angular/sdk-setup.ts b/src/angular/sdk-setup.ts index a3e8d239..31d38f06 100644 --- a/src/angular/sdk-setup.ts +++ b/src/angular/sdk-setup.ts @@ -26,12 +26,8 @@ export async function initalizeSentryOnApplicationEntry( if (hasSentryContent(originalAppEntry.$ast as t.Program)) { clack.log.warn( - `File ${chalk.cyan( - path.basename(appEntryPath), - )} already contains Sentry. -Skipping adding Sentry functionality to ${chalk.cyan( - path.basename(appEntryPath), - )}.`, + `File ${chalk.cyan(appEntryFilename)} already contains Sentry. +Skipping adding Sentry functionality to ${chalk.cyan(appEntryFilename)}.`, ); return; From 71f1f1abd79755dd40418d22638ddb2cb0408ccb Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 31 Jan 2025 15:43:42 +0000 Subject: [PATCH 4/4] Lint more --- src/angular/sdk-setup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/angular/sdk-setup.ts b/src/angular/sdk-setup.ts index 31d38f06..96eb8a5f 100644 --- a/src/angular/sdk-setup.ts +++ b/src/angular/sdk-setup.ts @@ -51,16 +51,16 @@ Skipping adding Sentry functionality to ${chalk.cyan(appEntryFilename)}.`, typeof error === 'object' && error != null && 'toString' in error ? error.toString() : typeof error === 'string' - ? error - : '', + ? error + : '', ), ); clack.log.warn( `Please refer to the documentation for manual setup: ${chalk.underline( - 'https://docs.sentry.io/platforms/javascript/guides/angular/#configure', - )}`, + 'https://docs.sentry.io/platforms/javascript/guides/angular/#configure', +)}`, ); return;