Skip to content

Commit

Permalink
feat(nuxt): Add sdk module to nuxt config and create client and serve…
Browse files Browse the repository at this point in the history
…r config files (#713)

* feat(nuxt): Add sdk module to nuxt config and create client and server config files

* Log snippets when user denies overwriting their configs
  • Loading branch information
andreiborza authored Nov 22, 2024
1 parent 7cf7927 commit e1b29d9
Show file tree
Hide file tree
Showing 5 changed files with 518 additions and 5 deletions.
1 change: 1 addition & 0 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum Integration {
cordova = 'cordova',
electron = 'electron',
nextjs = 'nextjs',
nuxt = 'nuxt',
remix = 'remix',
sveltekit = 'sveltekit',
sourcemaps = 'sourcemaps',
Expand Down
21 changes: 16 additions & 5 deletions src/nuxt/nuxt-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as clack from '@clack/prompts';
import * as Sentry from '@sentry/node';
import { lt, minVersion } from 'semver';
import type { WizardOptions } from '../utils/types';
import { withTelemetry } from '../telemetry';
import { traceStep, withTelemetry } from '../telemetry';
import {
abort,
abortIfCancelled,
Expand All @@ -16,6 +16,7 @@ import {
printWelcome,
} from '../utils/clack-utils';
import { getPackageVersion, hasPackageInstalled } from '../utils/package-json';
import { addSDKModule, getNuxtConfig, createConfigFiles } from './sdk-setup';

export function runNuxtWizard(options: WizardOptions) {
return withTelemetry(
Expand Down Expand Up @@ -71,10 +72,8 @@ export async function runNuxtWizardWithTelemetry(
}
}

const { authToken } = await getOrAskForProjectData(
options,
'javascript-nuxt',
);
const { authToken, selectedProject, selfHosted, sentryUrl } =
await getOrAskForProjectData(options, 'javascript-nuxt');

const sdkAlreadyInstalled = hasPackageInstalled('@sentry/nuxt', packageJson);
Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled);
Expand All @@ -85,4 +84,16 @@ export async function runNuxtWizardWithTelemetry(
});

await addDotEnvSentryBuildPluginFile(authToken);

const nuxtConfig = await traceStep('load-nuxt-config', getNuxtConfig);

await traceStep('configure-sdk', async () => {
await addSDKModule(nuxtConfig, {
org: selectedProject.organization.slug,
project: selectedProject.slug,
url: selfHosted ? sentryUrl : undefined,
});

await createConfigFiles(selectedProject.keys[0].dsn.public);
});
}
191 changes: 191 additions & 0 deletions src/nuxt/sdk-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import * as Sentry from '@sentry/node';
import chalk from 'chalk';
import fs from 'fs';
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
import { loadFile, generateCode } from 'magicast';
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
import { addNuxtModule } from 'magicast/helpers';
import path from 'path';
import {
getConfigBody,
getDefaultNuxtConfig,
getSentryConfigContents,
} from './templates';
import {
abort,
abortIfCancelled,
featureSelectionPrompt,
isUsingTypeScript,
} from '../utils/clack-utils';
import { traceStep } from '../telemetry';

const possibleNuxtConfig = [
'nuxt.config.js',
'nuxt.config.mjs',
'nuxt.config.cjs',
'nuxt.config.ts',
'nuxt.config.mts',
'nuxt.config.cts',
];

export async function getNuxtConfig(): Promise<string> {
let configFile = possibleNuxtConfig.find((fileName) =>
fs.existsSync(path.join(process.cwd(), fileName)),
);

if (!configFile) {
clack.log.info('No Nuxt config file found, creating a new one.');
Sentry.setTag('nuxt-config-strategy', 'create');
// nuxt recommends its config to be .ts by default
configFile = 'nuxt.config.ts';

await fs.promises.writeFile(
path.join(process.cwd(), configFile),
getDefaultNuxtConfig(),
{ encoding: 'utf-8', flag: 'w' },
);

clack.log.success(`Created ${chalk.cyan('nuxt.config.ts')}.`);
}

return path.join(process.cwd(), configFile);
}

export async function addSDKModule(
config: string,
options: { org: string; project: string; url?: string },
): Promise<void> {
clack.log.info('Adding Sentry Nuxt Module to Nuxt config.');

try {
const mod = await loadFile(config);

addNuxtModule(mod, '@sentry/nuxt/module', 'sentry', {
sourceMapsUploadOptions: {
org: options.org,
project: options.project,
...(options.url && { url: options.url }),
},
});
addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', { client: true });

const { code } = generateCode(mod);

await fs.promises.writeFile(config, code, { encoding: 'utf-8', flag: 'w' });
} catch (e: unknown) {
clack.log.error(
'Error while adding the Sentry Nuxt Module to the Nuxt config.',
);
clack.log.info(
chalk.dim(
typeof e === 'object' && e != null && 'toString' in e
? e.toString()
: typeof e === 'string'
? e
: 'Unknown error',
),
);
Sentry.captureException('Error while setting up the Nuxt SDK');
await abort('Exiting Wizard');
}
}

export async function createConfigFiles(dsn: string) {
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);

const typeScriptDetected = isUsingTypeScript();

const configVariants = ['server', 'client'] as const;

for (const configVariant of configVariants) {
await traceStep(`create-sentry-${configVariant}-config`, async () => {
const jsConfig = `sentry.${configVariant}.config.js`;
const tsConfig = `sentry.${configVariant}.config.ts`;

const jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig));
const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig));

let shouldWriteFile = true;

if (jsConfigExists || tsConfigExists) {
const existingConfigs = [];

if (jsConfigExists) {
existingConfigs.push(jsConfig);
}

if (tsConfigExists) {
existingConfigs.push(tsConfig);
}

const overwriteExistingConfigs = await abortIfCancelled(
clack.confirm({
message: `Found existing Sentry ${configVariant} config (${existingConfigs.join(
', ',
)}). Overwrite ${existingConfigs.length > 1 ? 'them' : 'it'}?`,
}),
);
Sentry.setTag(
`overwrite-${configVariant}-config`,
overwriteExistingConfigs,
);

shouldWriteFile = overwriteExistingConfigs;

if (overwriteExistingConfigs) {
if (jsConfigExists) {
fs.unlinkSync(path.join(process.cwd(), jsConfig));
clack.log.warn(`Removed existing ${chalk.cyan(jsConfig)}.`);
}
if (tsConfigExists) {
fs.unlinkSync(path.join(process.cwd(), tsConfig));
clack.log.warn(`Removed existing ${chalk.cyan(tsConfig)}.`);
}
}
}

if (shouldWriteFile) {
await fs.promises.writeFile(
path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig),
getSentryConfigContents(dsn, configVariant, selectedFeatures),
{ encoding: 'utf8', flag: 'w' },
);
clack.log.success(
`Created fresh ${chalk.cyan(
typeScriptDetected ? tsConfig : jsConfig,
)}.`,
);
Sentry.setTag(`created-${configVariant}-config`, true);
} else {
clack.log.info(
`Okay, here are the changes your ${chalk.cyan(
typeScriptDetected ? tsConfig : jsConfig,
)} should contain:`,
);
// eslint-disable-next-line no-console
console.log(
'\n\n ' +
getConfigBody(dsn, configVariant, selectedFeatures) +
'\n\n',
);
}
});
}
}
105 changes: 105 additions & 0 deletions src/nuxt/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
type SelectedSentryFeatures = {
performance: boolean;
replay: boolean;
};

export function getDefaultNuxtConfig(): string {
return `// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true }
})
`;
}

export function getSentryConfigContents(
dsn: string,
config: 'client' | 'server',
selectedFeatures: SelectedSentryFeatures,
): string {
if (config === 'client') {
return getSentryClientConfigContents(dsn, selectedFeatures);
}

return getSentryServerConfigContents(dsn, selectedFeatures);
}

const featuresConfigMap: Record<keyof SelectedSentryFeatures, string> = {
performance: [
' // We recommend adjusting this value in production, or using tracesSampler',
' // for finer control',
' tracesSampleRate: 1.0,',
].join('\n'),
replay: [
' // This sets the sample rate to be 10%. You may want this to be 100% while',
' // in development and sample at a lower rate in production',
' replaysSessionSampleRate: 0.1,',
' ',
' // If the entire session is not sampled, use the below sample rate to sample',
' // sessions when an error occurs.',
' replaysOnErrorSampleRate: 1.0,',
' ',
" // If you don't want to use Session Replay, just remove the line below:",
' integrations: [Sentry.replayIntegration()],',
].join('\n'),
};

const featuresMap: Record<
'client' | 'server',
Array<keyof SelectedSentryFeatures>
> = {
client: ['performance', 'replay'],
server: ['performance'],
};

export function getConfigBody(
dsn: string,
variant: 'client' | 'server',
selectedFeatures: SelectedSentryFeatures,
) {
return [
`dsn: "${dsn}",`,
Object.entries(selectedFeatures)
.map(([feature, activated]: [keyof SelectedSentryFeatures, boolean]) => {
return featuresMap[variant].includes(feature) && activated
? featuresConfigMap[feature]
: null;
})
.filter(Boolean)
.join('\n\n'),
]
.filter(Boolean)
.join('\n\n');
}

function getSentryClientConfigContents(
dsn: string,
selectedFeatures: SelectedSentryFeatures,
): string {
return `import * as Sentry from "@sentry/nuxt";
Sentry.init({
// If set up, you can use your runtime config here
// dsn: useRuntimeConfig().public.sentry.dsn,
${getConfigBody(dsn, 'client', selectedFeatures)}
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
`;
}

function getSentryServerConfigContents(
dsn: string,
selectedFeatures: SelectedSentryFeatures,
): string {
return `import * as Sentry from "@sentry/nuxt";
Sentry.init({
${getConfigBody(dsn, 'server', selectedFeatures)}
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
`;
}
Loading

0 comments on commit e1b29d9

Please sign in to comment.