Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(angular): Add Sentry setup in main.ts #768

Open
wants to merge 4 commits into
base: onur/angular-wizard-base
Choose a base branch
from
Open
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
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;
}
72 changes: 72 additions & 0 deletions src/angular/sdk-setup.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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)}`,
);
}
Loading