Skip to content

Commit

Permalink
feat(angular): Add Sentry setup in main.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
onurtemizkan committed Jan 22, 2025
1 parent a2b3649 commit b8544fa
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 1 deletion.
46 changes: 45 additions & 1 deletion src/angular/angular-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -99,6 +103,11 @@ ${chalk.underline(
return;
}

const { selectedProject } = await getOrAskForProjectData(
options,
'javascript-angular',
);

const sdkAlreadyInstalled = hasPackageInstalled(
'@sentry/angular',
packageJson,
Expand All @@ -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.',
)}`);
}
100 changes: 100 additions & 0 deletions src/angular/codemods/main.ts
Original file line number Diff line number Diff line change
@@ -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<any>,
dsn: string,
selectedFeatures: {
performance: boolean;
replay: boolean;
},
): ProxifiedModule<any> {
originalAppModuleMod.imports.$add({
from: '@sentry/angular',
imported: '*',
local: 'Sentry',
});

insertInitCall(originalAppModuleMod, dsn, selectedFeatures);

return originalAppModuleMod;
}

export function insertInitCall(
originalAppModuleMod: ProxifiedModule<any>,
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<string, unknown> {
const initCallArgs = {
dsn,
} as Record<string, unknown>;

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;
}
86 changes: 86 additions & 0 deletions src/angular/sdk-setup.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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)}`,
);
}

0 comments on commit b8544fa

Please sign in to comment.