diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 48a4e824..3d030a27 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -42,10 +42,11 @@ import { getSentryExamplePageContents, getSimpleUnderscoreErrorCopyPasteSnippet, getWithSentryConfigOptionsTemplate, + getSentryComponentContents, } from './templates'; import { traceStep, withTelemetry } from '../telemetry'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; -import { getNextJsVersionBucket } from './utils'; +import { getDevCommand, getNextJsVersionBucket } from './utils'; import { configureCI } from '../sourcemaps/sourcemaps-wizard'; export function runNextjsWizard(options: WizardOptions) { @@ -288,13 +289,6 @@ export async function runNextjsWizardWithTelemetry( } }); - const shouldCreateExamplePage = await askShouldCreateExamplePage(); - if (shouldCreateExamplePage) { - await traceStep('create-example-page', async () => - createExamplePage(selfHosted, selectedProject, sentryUrl), - ); - } - await addDotEnvSentryBuildPluginFile(authToken); const mightBeUsingVercel = fs.existsSync( @@ -309,12 +303,21 @@ export async function runNextjsWizardWithTelemetry( await traceStep('configure-ci', () => configureCI('nextjs', authToken)); } + const shouldCreateExamplePage = await askShouldCreateExamplePage(); + if (shouldCreateExamplePage) { + await traceStep('create-example-page', async () => + createExamplePage(selfHosted, selectedProject, sentryUrl), + ); + } + clack.outro(` ${chalk.green('Successfully installed the Sentry Next.js SDK!')} ${ shouldCreateExamplePage ? `\n\nYou can validate your setup by restarting your dev environment (${chalk.cyan( - `next dev`, - )}) and visiting ${chalk.cyan('"/sentry-example-page"')}` + getDevCommand(), + )}) and visiting ${chalk.cyan( + 'http://localhost:3000/sentry-example-page', + )}` : '' } @@ -745,122 +748,134 @@ async function createExamplePage( } if (appFolderLocation) { - const examplePageContents = getSentryExamplePageContents({ + await writeExamplePageContents({ + appRouter: true, + apiRoute: getSentryExampleAppDirApiRoute(), selfHosted, orgSlug: selectedProject.organization.slug, projectId: selectedProject.id, sentryUrl, - useClient: true, + folderLocation: appFolderLocation, + typeScriptDetected, }); - - fs.mkdirSync( - path.join(process.cwd(), ...appFolderLocation, 'sentry-example-page'), - { - recursive: true, - }, - ); - - const newPageFileName = `page.${typeScriptDetected ? 'tsx' : 'jsx'}`; - - await fs.promises.writeFile( - path.join( - process.cwd(), - ...appFolderLocation, - 'sentry-example-page', - newPageFileName, - ), - examplePageContents, - { encoding: 'utf8', flag: 'w' }, - ); - - clack.log.success( - `Created ${chalk.cyan( - path.join(...appFolderLocation, 'sentry-example-page', newPageFileName), - )}.`, - ); - - fs.mkdirSync( - path.join( - process.cwd(), - ...appFolderLocation, - 'api', - 'sentry-example-api', - ), - { - recursive: true, - }, - ); - - const newRouteFileName = `route.${typeScriptDetected ? 'ts' : 'js'}`; - - await fs.promises.writeFile( - path.join( - process.cwd(), - ...appFolderLocation, - 'api', - 'sentry-example-api', - newRouteFileName, - ), - getSentryExampleAppDirApiRoute(), - { encoding: 'utf8', flag: 'w' }, - ); - - clack.log.success( - `Created ${chalk.cyan( - path.join( - ...appFolderLocation, - 'api', - 'sentry-example-api', - newRouteFileName, - ), - )}.`, - ); } else if (pagesFolderLocation) { - const examplePageContents = getSentryExamplePageContents({ + await writeExamplePageContents({ + appRouter: false, + apiRoute: getSentryExamplePagesDirApiRoute(), selfHosted, orgSlug: selectedProject.organization.slug, projectId: selectedProject.id, sentryUrl, - useClient: false, + folderLocation: pagesFolderLocation, + typeScriptDetected, }); + } +} - await fs.promises.writeFile( - path.join( - process.cwd(), - ...pagesFolderLocation, - 'sentry-example-page.jsx', - ), - examplePageContents, - { encoding: 'utf8', flag: 'w' }, - ); +/** + * Create all necessary files for the example page + */ +async function writeExamplePageContents({ + appRouter, + apiRoute, + folderLocation, + projectId, + selfHosted, + sentryUrl, + typeScriptDetected, + orgSlug, +}: { + appRouter: boolean; + selfHosted: boolean; + orgSlug: string; + projectId: string; + sentryUrl: string; + apiRoute: string; + folderLocation: string[]; + typeScriptDetected: boolean; +}) { + const examplePageDirName = 'sentry-example-page'; + const examplePageComponentsDirName = 'components'; + + const examplePageContents = getSentryExamplePageContents({ + useClient: appRouter, + }); - clack.log.success( - `Created ${chalk.cyan( - path.join(...pagesFolderLocation, 'sentry-example-page.js'), - )}.`, - ); + fs.mkdirSync( + path.join( + process.cwd(), + ...folderLocation, + examplePageDirName, + examplePageComponentsDirName, + ), + { + recursive: true, + }, + ); - fs.mkdirSync(path.join(process.cwd(), ...pagesFolderLocation, 'api'), { + const newPageFileName = `page.${typeScriptDetected ? 'tsx' : 'jsx'}`; + + await fs.promises.writeFile( + path.join( + process.cwd(), + ...folderLocation, + examplePageDirName, + newPageFileName, + ), + examplePageContents, + { encoding: 'utf8', flag: 'w' }, + ); + + const exampleComponentContents = getSentryComponentContents({ + useClient: appRouter, + orgSlug, + selfHosted, + sentryUrl, + projectId, + }); + + const componentFileName = `sentryPage.${typeScriptDetected ? 'tsx' : 'jsx'}`; + + await fs.promises.writeFile( + path.join( + process.cwd(), + ...folderLocation, + examplePageDirName, + examplePageComponentsDirName, + componentFileName, + ), + exampleComponentContents, + { encoding: 'utf8', flag: 'w' }, + ); + + clack.log.success( + `Created ${chalk.cyan( + path.join(...folderLocation, 'sentry-example-page', newPageFileName), + )}.`, + ); + + fs.mkdirSync( + path.join(process.cwd(), ...folderLocation, 'api', 'sentry-example-api'), + { recursive: true, - }); + }, + ); - await fs.promises.writeFile( - path.join( - process.cwd(), - ...pagesFolderLocation, - 'api', - 'sentry-example-api.js', - ), - getSentryExamplePagesDirApiRoute(), - { encoding: 'utf8', flag: 'w' }, - ); + const newRouteFileName = `route.${typeScriptDetected ? 'ts' : 'js'}`; + + await fs.promises.writeFile( + path.join( + process.cwd(), + ...folderLocation, + 'api', + 'sentry-example-api', + newRouteFileName, + ), + apiRoute, + { encoding: 'utf8', flag: 'w' }, + ); - clack.log.success( - `Created ${chalk.cyan( - path.join(...pagesFolderLocation, 'api', 'sentry-example-api.js'), - )}.`, - ); - } + return newRouteFileName; } /** diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index a36eefe0..671be4ae 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -189,6 +189,29 @@ Sentry.init({ } export function getSentryExamplePageContents(options: { + useClient: boolean; +}): string { + return `${ + options.useClient ? '"use client";\n\n' : '' + }import SentryPage from "./components/sentryPage"; + + export default function Page() { + const onButtonClick = async () => { + // this will throw + JSON.parse("Oh oh"); + const res = await fetch("/api/sentry-example-api"); + if (!res.ok) { + throw new Error("Sentry Example Frontend Error"); + } + }; + + return ; + } + + `; +} + +export function getSentryComponentContents(options: { selfHosted: boolean; sentryUrl: string; orgSlug: string; @@ -199,19 +222,65 @@ export function getSentryExamplePageContents(options: { ? `${options.sentryUrl}organizations/${options.orgSlug}/issues/?project=${options.projectId}` : `https://${options.orgSlug}.sentry.io/issues/?project=${options.projectId}`; - return `${ + return `// @ts-nocheck + ${ options.useClient ? '"use client";\n\n' : '' - }import Head from "next/head"; -import * as Sentry from "@sentry/nextjs"; + }import { useRef, useState } from "react"; + import Link from "next/link"; -export default function Page() { - return ( -
- - Sentry Onboarding - - +export default function SentryPage({ onButtonClick }) { + const canvasRef = useRef(null); + const [link, setLink] = useState(""); + + const handleClick = async (clickX, clickY) => { + const canvas = canvasRef.current; + if (canvas) { + explode(canvas, clickX, clickY); + } + + setLink("${issuesPageLink}"); + setTimeout(() => { + canvasRef.current?.style.setProperty("z-index", "0"); + }, 2000); + + onButtonClick(); + }; + + return ( + <> + + + + +
-

+

- -

Get started by sending us a sample error:

-
+
- Throw error! - - -

- Next, look for the error on the{" "} - Issues Page. -

-

- For more information, see{" "} - - https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -

+
+

+ Test your Sentry SDK Setup +

+ {link ? ( +
+ Good job, now head over to{" "} + + {link} + +
+ ) : ( + <> +

+ First, simulate throwing an error: +

+
+ +
+ + )} +
+
- + ); } -`; + +// @ts-nocheck + +const PeekingGremlin = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const GremlinWrapper = ({ children }) => ( +
+ {children} +
+); + +const Gremlin = ({ children }) => ( +
+ {children} +
+); + +const Hands = ({ children }) => ( +
+ {children} +
+); + +export const explode = (canvas, initialX, initialY) => { + const ctx = canvas.getContext("2d"); + if (ctx) { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const config = { + particleNumber: 1000, + maxParticleSize: 14, + maxSpeed: 50, + colorVariation: 50, + }; + + const colorPalette = { + matter: [ + { r: 241, g: 183, b: 28 }, + { r: 106, g: 95, b: 193 }, + { r: 255, g: 89, b: 128 }, + { r: 241, g: 183, b: 28 }, + ], + }; + + let particles = []; + + const clear = function (ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; + + const createParticle = function (newX, newY) { + return { + x: newX || Math.round(Math.random() * canvas.width), + y: newY || Math.round(Math.random() * canvas.height), + r: Math.ceil(Math.random() * config.maxParticleSize), + c: colorVariation( + colorPalette.matter[ + Math.floor(Math.random() * colorPalette.matter.length) + ], + true + ), + s: Math.pow(Math.ceil(Math.random() * config.maxSpeed), 0.7), + d: Math.round(Math.random() * 360), + }; + }; + + const colorVariation = function (color, returnString) { + var r, g, b, a, variation; + r = Math.round( + Math.random() * config.colorVariation - + config.colorVariation / 2 + + color.r + ); + g = Math.round( + Math.random() * config.colorVariation - + config.colorVariation / 2 + + color.g + ); + b = Math.round( + Math.random() * config.colorVariation - + config.colorVariation / 2 + + color.b + ); + a = Math.random() + 0.5; + if (returnString) { + return "rgba(" + r + "," + g + "," + b + "," + a + ")"; + } else { + return { r, g, b, a }; + } + }; + + const updateParticleModel = function (p) { + var a = 180 - (p.d + 90); + p.d > 0 && p.d < 180 + ? (p.x += (p.s * Math.sin(p.d)) / Math.sin(p.s)) + : (p.x -= (p.s * Math.sin(p.d)) / Math.sin(p.s)); + p.d > 90 && p.d < 270 + ? (p.y += (p.s * Math.sin(a)) / Math.sin(p.s)) + : (p.y -= (p.s * Math.sin(a)) / Math.sin(p.s)); + return p; + }; + + const drawParticle = function (x, y, r, c) { + ctx.beginPath(); + ctx.fillStyle = c; + ctx.arc(x, y, r, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.closePath(); + }; + + const cleanUpArray = function () { + particles = particles.filter((p) => { + return p.x > -100 && p.y > -100; + }); + }; + + const initParticles = function (numParticles, x, y) { + for (let i = 0; i < numParticles; i++) { + particles.push(createParticle(x, y)); + } + particles.forEach((p) => { + drawParticle(p.x, p.y, p.r, p.c); + }); + }; + + const frame = function () { + clear(ctx); + particles.map((p) => { + return updateParticleModel(p); + }); + particles.forEach((p) => { + drawParticle(p.x, p.y, p.r, p.c); + }); + + window.requestAnimationFrame(frame); + }; + + frame(); + + let nextX = initialX; + let nextY = initialY; + + initParticles(config.particleNumber, nextX, nextY); + cleanUpArray(); + setTimeout(() => { + nextX += Math.floor(Math.random() * 101) - 50; + nextY += Math.floor(Math.random() * 101) - 50; + initParticles(config.particleNumber, nextX, nextY); + cleanUpArray(); + }, 500); + } +}; + + `; } export function getSentryExamplePagesDirApiRoute() { diff --git a/src/nextjs/utils.ts b/src/nextjs/utils.ts index 7e50d6de..913611ba 100644 --- a/src/nextjs/utils.ts +++ b/src/nextjs/utils.ts @@ -1,4 +1,5 @@ import { major, minVersion } from 'semver'; +import { detectPackageManger } from '../utils/package-manager'; export function getNextJsVersionBucket(version: string | undefined) { if (!version) { @@ -19,3 +20,11 @@ export function getNextJsVersionBucket(version: string | undefined) { return 'unknown'; } } + +export function getDevCommand() { + const manager = detectPackageManger(); + if (manager) { + return `${manager.devCommand}`; + } + return 'next dev'; +} diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 183b9a3d..9ac68baf 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -13,6 +13,8 @@ export interface PackageManager { buildCommand: string; /* The command that the package manager uses to run a script from package.json */ runScriptCommand: string; + /* The command that the package manager uses to run a dev server from package.json */ + devCommand: string; flags: string; } @@ -23,6 +25,7 @@ export const BUN: PackageManager = { installCommand: 'bun add', buildCommand: 'bun run build', runScriptCommand: 'bun run', + devCommand: 'bun run dev', flags: '', }; export const YARN: PackageManager = { @@ -32,6 +35,7 @@ export const YARN: PackageManager = { installCommand: 'yarn add', buildCommand: 'yarn build', runScriptCommand: 'yarn', + devCommand: 'yarn dev', flags: '--ignore-workspace-root-check', }; export const PNPM: PackageManager = { @@ -41,6 +45,7 @@ export const PNPM: PackageManager = { installCommand: 'pnpm add', buildCommand: 'pnpm build', runScriptCommand: 'pnpm', + devCommand: 'pnpm run dev', flags: '--ignore-workspace-root-check', }; export const NPM: PackageManager = { @@ -50,6 +55,7 @@ export const NPM: PackageManager = { installCommand: 'npm add', buildCommand: 'npm run build', runScriptCommand: 'npm run', + devCommand: 'npm run dev', flags: '', };