Skip to content

Commit

Permalink
feat(angular): Add Sentry setup in App Config (#769)
Browse files Browse the repository at this point in the history
* feat(angular): Add Sentry setup in App Config

* Use `hasSentryContent` from `ast-utils`

* Bail out if `app-config` is not under pre-defined location
  • Loading branch information
onurtemizkan authored Feb 4, 2025
1 parent 49dda83 commit 0d8936d
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 11 deletions.
4 changes: 4 additions & 0 deletions src/angular/angular-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { gte, minVersion, SemVer } from 'semver';

import * as Sentry from '@sentry/node';
import { initalizeSentryOnApplicationEntry } from './sdk-setup';
import { updateAppConfig } from './sdk-setup';

const MIN_SUPPORTED_ANGULAR_VERSION = '14.0.0';

Expand Down Expand Up @@ -155,4 +156,7 @@ ${chalk.underline(
${chalk.green(
'Sentry has been successfully configured for your Angular project.',
)}`);
await traceStep('Update Angular project configuration', async () => {
await updateAppConfig(installedMinVersion, selectedFeatures.performance);
});
}
265 changes: 265 additions & 0 deletions src/angular/codemods/app-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

import type { ArrayExpression, Identifier, ObjectProperty } from '@babel/types';

// @ts-expect-error - magicast is ESM and TS complains about that. It works though
import type { ProxifiedModule } from 'magicast';

// @ts-ignore - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import { gte, type SemVer } from 'semver';
import * as recast from 'recast';
import chalk from 'chalk';

export function updateAppConfigMod(
originalAppConfigMod: ProxifiedModule<any>,
angularVersion: SemVer,
isTracingEnabled: boolean,
): ProxifiedModule<any> {
const isAboveAngularV19 = gte(angularVersion, '19.0.0');

addImports(originalAppConfigMod, isAboveAngularV19, isTracingEnabled);
addProviders(originalAppConfigMod, isAboveAngularV19, isTracingEnabled);

return originalAppConfigMod;
}

function addSentryImport(originalAppConfigMod: ProxifiedModule<any>): void {
const imports = originalAppConfigMod.imports;
const hasSentryImport = imports.$items.some(
(item) => item.from === '@sentry/angular',
);

if (!hasSentryImport) {
imports.$add({
from: '@sentry/angular',
imported: '*',
local: 'Sentry',
});
}
}

function addErrorHandlerImport(
originalAppConfigMod: ProxifiedModule<any>,
): void {
const imports = originalAppConfigMod.imports;
const hasErrorHandler = imports.$items.some(
(item) => item.local === 'ErrorHandler' && item.from === '@angular/core',
);

if (!hasErrorHandler) {
imports.$add({
from: '@angular/core',
imported: 'ErrorHandler',
local: 'ErrorHandler',
});
}
}

function addRouterImport(originalAppConfigMod: ProxifiedModule<any>): void {
const imports = originalAppConfigMod.imports;
const hasRouter = imports.$items.some(
(item) => item.local === 'Router' && item.from === '@angular/router',
);

if (!hasRouter) {
imports.$add({
from: '@angular/router',
imported: 'Router',
local: 'Router',
});
}
}

function addMissingImportsV19(
originalAppConfigMod: ProxifiedModule<any>,
): void {
const imports = originalAppConfigMod.imports;

const hasProvideAppInitializer = imports.$items.some(
(item) =>
item.local === 'provideAppInitializer' && item.from === '@angular/core',
);

if (!hasProvideAppInitializer) {
imports.$add({
from: '@angular/core',
imported: 'provideAppInitializer',
local: 'provideAppInitializer',
});
}

const hasInject = imports.$items.some(
(item) => item.local === 'inject' && item.from === '@angular/core',
);

if (!hasInject) {
imports.$add({
from: '@angular/core',
imported: 'inject',
local: 'inject',
});
}
}

function addAppInitializer(originalAppConfigMod: ProxifiedModule<any>): void {
const imports = originalAppConfigMod.imports;

const hasAppInitializer = imports.$items.some(
(item) => item.local === 'APP_INITIALIZER' && item.from === '@angular/core',
);

if (!hasAppInitializer) {
imports.$add({
from: '@angular/core',
imported: 'APP_INITIALIZER',
local: 'APP_INITIALIZER',
});
}
}

function addImports(
originalAppConfigMod: ProxifiedModule<any>,
isAboveAngularV19: boolean,
isTracingEnabled: boolean,
): void {
addSentryImport(originalAppConfigMod);
addErrorHandlerImport(originalAppConfigMod);

if (isTracingEnabled) {
addRouterImport(originalAppConfigMod);
}

if (isAboveAngularV19) {
addMissingImportsV19(originalAppConfigMod);
} else if (isTracingEnabled) {
addAppInitializer(originalAppConfigMod);
}
}

function addProviders(
originalAppConfigMod: ProxifiedModule<any>,
isAboveAngularV19: boolean,
isTracingEnabled: boolean,
): void {
const b = recast.types.builders;

recast.visit(originalAppConfigMod.exports.$ast, {
visitExportNamedDeclaration(path) {
// @ts-expect-error - declaration should always be present in this case
if (path.node.declaration.declarations[0].id.name === 'appConfig') {
const appConfigProps =
// @ts-expect-error - declaration should always be present in this case
path.node.declaration.declarations[0].init.properties;

const providers = appConfigProps.find(
(prop: ObjectProperty) =>
(prop.key as Identifier).name === 'providers',
).value as ArrayExpression;

// Check if there is already an ErrorHandler provider
const hasErrorHandlerProvider = providers.elements.some(
(element) =>
element &&
element.type === 'ObjectExpression' &&
element.properties.some(
(prop) =>
prop.type === 'ObjectProperty' &&
(prop.key as Identifier).name === 'provide' &&
(prop.value as Identifier).name === 'ErrorHandler',
),
);

// If there is already an ErrorHandler provider, we skip adding it and log a message
if (hasErrorHandlerProvider) {
clack.log
.warn(`ErrorHandler provider already exists in your app config.
Please refer to the Sentry Angular SDK documentation to combine it manually with Sentry's ErrorHandler.
${chalk.underline(
'https://docs.sentry.io/platforms/javascript/guides/angular/features/error-handler/',
)}
`);
} else {
const errorHandlerObject = b.objectExpression([
b.objectProperty(
b.identifier('provide'),
b.identifier('ErrorHandler'),
),
b.objectProperty(
b.identifier('useValue'),
b.identifier('Sentry.createErrorHandler()'),
),
]);

providers.elements.push(
// @ts-expect-error - errorHandlerObject is an objectExpression
errorHandlerObject,
);
}

if (isTracingEnabled) {
const traceServiceObject = b.objectExpression([
b.objectProperty(
b.identifier('provide'),
b.identifier('Sentry.TraceService'),
),
b.objectProperty(
b.identifier('deps'),
b.arrayExpression([b.identifier('Router')]),
),
]);

// @ts-expect-error - traceServiceObject is an objectExpression
providers.elements.push(traceServiceObject);

if (isAboveAngularV19) {
const provideAppInitializerCall = b.callExpression(
b.identifier('provideAppInitializer'),
[
b.arrowFunctionExpression(
[],
b.blockStatement([
b.expressionStatement(
b.callExpression(b.identifier('inject'), [
b.identifier('Sentry.TraceService'),
]),
),
]),
),
],
);

// @ts-expect-error - provideAppInitializerCall is an objectExpression
providers.elements.push(provideAppInitializerCall);
} else {
const provideAppInitializerObject = b.objectExpression([
b.objectProperty(
b.identifier('provide'),
b.identifier('APP_INITIALIZER'),
),
b.objectProperty(
b.identifier('useFactory'),
b.arrowFunctionExpression(
[],
b.arrowFunctionExpression([], b.blockStatement([])),
),
),
b.objectProperty(
b.identifier('deps'),
b.arrayExpression([b.identifier('Sentry.TraceService')]),
),
b.objectProperty(b.identifier('multi'), b.booleanLiteral(true)),
]);

// @ts-expect-error - provideAppInitializerObject is an objectExpression
providers.elements.push(provideAppInitializerObject);
}
}
}

this.traverse(path);
},
});
}
Loading

0 comments on commit 0d8936d

Please sign in to comment.