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(nuxt): Add deployment-platform flow with links to docs #747

Merged
merged 5 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- feat(nuxt): Add `import-in-the-middle` install step when using pnpm ([#727](https://github.com/getsentry/sentry-wizard/pull/727))
- fix(nuxt): Remove unused parameter in sentry-example-api template ([#734](https://github.com/getsentry/sentry-wizard/pull/734))
- fix(nuxt): Remove option to downgrade override nitropack ([#744](https://github.com/getsentry/sentry-wizard/pull/744))
- feat(nuxt): Add deployment-platform flow with links to docs ([#747](https://github.com/getsentry/sentry-wizard/pull/747))

## 3.36.0

Expand Down
12 changes: 11 additions & 1 deletion e2e-tests/tests/nuxt-3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,18 @@
},
));

const tracingOptionPrompted =
const deploymentPlatformPrompted =
nftOverridePrompted &&
(await wizardInstance.sendStdinAndWaitForOutput(
KEYS.ENTER,
'Please select your deployment platform.',
{
timeout: 240_000,
},
));

const tracingOptionPrompted =
deploymentPlatformPrompted &&
(await wizardInstance.sendStdinAndWaitForOutput(
KEYS.ENTER,
// "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold.
Expand Down Expand Up @@ -94,27 +104,27 @@
function testNuxtProjectSetup(projectDir: string) {
const integration = Integration.nuxt;

test('package.json is updated correctly', () => {

Check warning on line 107 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkPackageJson(projectDir, integration);
});

test('.env-sentry-build-plugin is created and contains the auth token', () => {

Check warning on line 111 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkEnvBuildPlugin(projectDir);
});

test('config files created', () => {

Check warning on line 115 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkFileExists(`${projectDir}/sentry.server.config.ts`);
checkFileExists(`${projectDir}/sentry.client.config.ts`);
});

test('example page exists', () => {

Check warning on line 120 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkFileExists(`${projectDir}/pages/sentry-example-page.vue`);
checkFileExists(`${projectDir}/server/api/sentry-example-api.ts`);
});
}

function testNuxtProjectConfigs(projectDir: string) {
test('nuxt config contains sentry module', () => {

Check warning on line 127 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkFileContents(path.resolve(projectDir, 'nuxt.config.ts'), [
"modules: ['@sentry/nuxt/module'],",
'sentry: {',
Expand All @@ -129,7 +139,7 @@
]);
});

test('sentry.client.config.ts contents', () => {

Check warning on line 142 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkFileContents(path.resolve(projectDir, 'sentry.client.config.ts'), [
'import * as Sentry from "@sentry/nuxt";',
'Sentry.init({',
Expand All @@ -153,7 +163,7 @@
]);
});

test('sentry.server.config.ts contents', () => {

Check warning on line 166 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkFileContents(path.resolve(projectDir, 'sentry.server.config.ts'), [
'import * as Sentry from "@sentry/nuxt";',
'Sentry.init({',
Expand All @@ -169,11 +179,11 @@
}

function testNuxtProjectBuildsAndRuns(projectDir: string) {
test('builds successfully', async () => {

Check warning on line 182 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
await checkIfBuilds(projectDir, 'preview this build');
});

test('runs on prod mode correctly', async () => {

Check warning on line 186 in e2e-tests/tests/nuxt-3.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
await checkIfRunsOnProdMode(projectDir, 'Listening on');
});
}
12 changes: 11 additions & 1 deletion e2e-tests/tests/nuxt-4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,18 @@
},
));

const tracingOptionPrompted =
const deploymentPlatformPrompted =
nftOverridePrompted &&
(await wizardInstance.sendStdinAndWaitForOutput(
KEYS.ENTER,
'Please select your deployment platform.',
{
timeout: 240_000,
},
));

const tracingOptionPrompted =
deploymentPlatformPrompted &&
(await wizardInstance.sendStdinAndWaitForOutput(
KEYS.ENTER,
// "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold.
Expand Down Expand Up @@ -93,7 +103,7 @@
function testNuxtProjectSetup(projectDir: string) {
const integration = Integration.nuxt;

test('package.json is updated correctly', () => {

Check warning on line 106 in e2e-tests/tests/nuxt-4.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Test has no assertions
checkPackageJson(projectDir, integration);
});

Expand Down
29 changes: 25 additions & 4 deletions src/nuxt/nuxt-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import {
getNuxtConfig,
createConfigFiles,
addNuxtOverrides,
askDeploymentPlatform,
} from './sdk-setup';
import {
createExampleComponent,
createExamplePage,
supportsExamplePage,
} from './sdk-example';
import { isNuxtV4 } from './utils';
import { DeploymentPlatform } from './types';

export function runNuxtWizard(options: WizardOptions) {
return withTelemetry(
Expand Down Expand Up @@ -116,8 +118,10 @@ export async function runNuxtWizardWithTelemetry(
selfHosted,
};

const deploymentPlatform = await askDeploymentPlatform();

await traceStep('configure-sdk', async () => {
await addSDKModule(nuxtConfig, projectData);
await addSDKModule(nuxtConfig, projectData, deploymentPlatform);
await createConfigFiles(selectedProject.keys[0].dsn.public);
});

Expand Down Expand Up @@ -149,16 +153,32 @@ export async function runNuxtWizardWithTelemetry(
await runPrettierIfInstalled();

clack.outro(
buildOutroMessage(shouldCreateExamplePage, shouldCreateExampleButton),
buildOutroMessage(
shouldCreateExamplePage,
shouldCreateExampleButton,
deploymentPlatform,
),
);
}

function buildOutroMessage(
shouldCreateExamplePage: boolean,
shouldCreateExampleButton: boolean,
deploymentPlatform: DeploymentPlatform | symbol,
): string {
const canImportSentryServerConfigFile =
deploymentPlatform !== 'vercel' && deploymentPlatform !== 'netlify';

let msg = chalk.green('\nSuccessfully installed the Sentry Nuxt SDK!');

if (canImportSentryServerConfigFile) {
msg += `\n\nAfter building your Nuxt app, you need to ${chalk.cyan(
'--import',
)} the Sentry server config file.\n\nFor more info see: ${chalk.cyan(
andreiborza marked this conversation as resolved.
Show resolved Hide resolved
'https://docs.sentry.io/platforms/javascript/guides/nuxt/install/cli-import/#initializing-sentry-with---import',
)}`;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: given that this is a pretty crucial step, I was thinking that we could make this a step that users have to confirm instead of showing this in the outro message.

So basically, print this message and make them confirm the step with an "Ok, I got it" or so. There's similar precedence for something like this in our source map wizard.

I'll leave this up to you though, so feel free to go with whatever you prefer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good idea! What if we (also?) opened up the docs for them with that link since it's pretty crucial that they read the info on there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lms24 @s1gr1d how about this?
Screenshot 2024-12-19 at 09 43 26@2x

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! I think it's most important to have this one extra step so that people actually read what the wizard tells them, before the outro message is printed. Asking about opening docs achieves this well. Thanks!


if (shouldCreateExamplePage) {
msg += `\n\nYou can validate your setup by visiting ${chalk.cyan(
'"/sentry-example-page"',
Expand All @@ -170,8 +190,9 @@ function buildOutroMessage(
)} component to a page and triggering it.`;
}

msg += `\n\nCheck out the SDK documentation for further configuration:
https://docs.sentry.io/platforms/javascript/guides/nuxt/`;
msg += `\n\nCheck out the SDK documentation for further configuration: ${chalk.cyan(
'https://docs.sentry.io/platforms/javascript/guides/nuxt/',
)}`;

return msg;
}
91 changes: 61 additions & 30 deletions src/nuxt/sdk-setup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import * as 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, MagicastError } from 'magicast';
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';
Expand All @@ -15,7 +15,6 @@ import {
getSentryConfigContents,
} from './templates';
import {
abort,
abortIfCancelled,
askShouldAddPackageOverride,
askShouldInstallPackage,
Expand All @@ -27,6 +26,7 @@ import { traceStep } from '../telemetry';
import { lt, SemVer } from 'semver';
import { PackageManager, PNPM } from '../utils/package-manager';
import { hasPackageInstalled, PackageDotJson } from '../utils/package-json';
import { deploymentPlatforms, DeploymentPlatform } from './types';

const possibleNuxtConfig = [
'nuxt.config.js',
Expand Down Expand Up @@ -60,10 +60,40 @@ export async function getNuxtConfig(): Promise<string> {
return path.join(process.cwd(), configFile);
}

export async function askDeploymentPlatform(): Promise<
DeploymentPlatform | symbol
> {
return await abortIfCancelled(
clack.select({
message: 'Please select your deployment platform.',
options: deploymentPlatforms.map((platform) => ({
value: platform,
label: `${platform.charAt(0).toUpperCase()}${platform.slice(1)}`,
})),
}),
);
}

export async function addSDKModule(
config: string,
options: { org: string; project: string; url: string; selfHosted: boolean },
deploymentPlatform: DeploymentPlatform | symbol,
): Promise<void> {
const shouldTopLevelImport =
deploymentPlatform === 'vercel' || deploymentPlatform === 'netlify';

if (shouldTopLevelImport) {
clack.log.warn(
`Sentry needs to be initialized before the application starts. ${chalk.cyan(
`${deploymentPlatform
.charAt(0)
.toUpperCase()}${deploymentPlatform.slice(1)}`,
)} does not support this yet.\n\nWe will inject the Sentry server-side config at the top of your Nuxt server entry file instead.\n\nThis comes with some restrictions, for more info see:\n\n${chalk.cyan(
'https://docs.sentry.io/platforms/javascript/guides/nuxt/install/top-level-import/',
)} `,
);
}

try {
const mod = await loadFile(config);

Expand All @@ -73,6 +103,9 @@ export async function addSDKModule(
project: options.project,
...(options.selfHosted && { url: options.url }),
},
...(shouldTopLevelImport && {
autoInjectServerSentry: 'top-level-import',
}),
});
addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', {
client: 'hidden',
Expand All @@ -86,33 +119,31 @@ export async function addSDKModule(
`Added Sentry Nuxt Module to ${chalk.cyan(path.basename(config))}.`,
);
} catch (e: unknown) {
// Cases where users spread options are not covered by magicast,
// so we fall back to showing how to configure the nuxt config
// manually.
if (e instanceof MagicastError) {
clack.log.warn(
`Automatic configuration of ${chalk.cyan(
path.basename(config),
)} failed, please add the following settings:`,
);
// eslint-disable-next-line no-console
console.log(`\n\n${getNuxtModuleFallbackTemplate(options)}\n\n`);
} else {
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');
}
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 Module in nuxt config',
);

clack.log.warn(
`Please add the following settings to ${chalk.cyan(
path.basename(config),
)}:`,
);
// eslint-disable-next-line no-console
console.log(
`\n\n${getNuxtModuleFallbackTemplate(options, shouldTopLevelImport)}\n\n`,
);
}
}

Expand Down
21 changes: 14 additions & 7 deletions src/nuxt/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,27 @@ export default defineNuxtConfig({
`;
}

export function getNuxtModuleFallbackTemplate(options: {
org: string;
project: string;
url: string;
selfHosted: boolean;
}): string {
export function getNuxtModuleFallbackTemplate(
options: {
org: string;
project: string;
url: string;
selfHosted: boolean;
},
shouldTopLevelImport: boolean,
): string {
return ` modules: ["@sentry/nuxt/module"],
sentry: {
sourceMapsUploadOptions: {
org: "${options.org}",
project: "${options.project}",${
options.selfHosted ? `\n url: "${options.url}",` : ''
}
},
},${
shouldTopLevelImport
? `\n autoInjectServerSentry: "top-level-import",`
: ''
}
},
sourcemap: { client: "hidden" },`;
}
Expand Down
8 changes: 8 additions & 0 deletions src/nuxt/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const deploymentPlatforms = [
'vercel',
'netlify',
'other',
'none',
] as const;

export type DeploymentPlatform = (typeof deploymentPlatforms)[number];
24 changes: 17 additions & 7 deletions src/nuxt/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// @ts-ignore - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import { gte, minVersion } from 'semver';
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
import { loadFile } from 'magicast';
import { abortIfCancelled } from '../utils/clack-utils';

export async function isNuxtV4(
nuxtConfig: string,
Expand All @@ -18,14 +21,21 @@ export async function isNuxtV4(
// At the time of writing, nuxt 4 is not on its own
// major yet. We must read the `compatibilityVersion`
// from the nuxt config.
const mod = await loadFile(nuxtConfig);
const config =
mod.exports.default.$type === 'function-call'
? mod.exports.default.$args[0]
: mod.exports.default;
try {
const mod = await loadFile(nuxtConfig);
const config =
mod.exports.default.$type === 'function-call'
? mod.exports.default.$args[0]
: mod.exports.default;

if (config && config.future && config.future.compatibilityVersion === 4) {
return true;
if (config && config.future && config.future.compatibilityVersion === 4) {
return true;
}
} catch {
// If we cannot parse their config, just ask.
return await abortIfCancelled(
clack.confirm({ message: 'Are you using Nuxt version 4?' }),
);
}

return false;
Expand Down
39 changes: 33 additions & 6 deletions test/nuxt/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,38 @@ describe('Nuxt code templates', () => {

describe('getNuxtModuleFallbackTemplate', () => {
it('generates configuration options for the nuxt config', () => {
const template = getNuxtModuleFallbackTemplate({
org: 'my-org',
project: 'my-project',
url: 'https://sentry.io',
selfHosted: false,
});
const template = getNuxtModuleFallbackTemplate(
{
org: 'my-org',
project: 'my-project',
url: 'https://sentry.io',
selfHosted: false,
},
false,
);

expect(template).toMatchInlineSnapshot(`
" modules: ["@sentry/nuxt/module"],
sentry: {
sourceMapsUploadOptions: {
org: "my-org",
project: "my-project",
},
},
sourcemap: { client: "hidden" },"
`);
});

it('generates configuration options for the nuxt config with top level import', () => {
const template = getNuxtModuleFallbackTemplate(
{
org: 'my-org',
project: 'my-project',
url: 'https://sentry.io',
selfHosted: false,
},
true,
);

expect(template).toMatchInlineSnapshot(`
" modules: ["@sentry/nuxt/module"],
Expand All @@ -220,6 +246,7 @@ describe('Nuxt code templates', () => {
org: "my-org",
project: "my-project",
},
autoInjectServerSentry: "top-level-import",
},
sourcemap: { client: "hidden" },"
`);
Expand Down
Loading