From 3184bf25bc645a889b74e88214e577f9eefa4920 Mon Sep 17 00:00:00 2001 From: Ryan Lockard <25166787+realStandal@users.noreply.github.com> Date: Thu, 11 May 2023 02:23:08 -0400 Subject: [PATCH] Add `setup sentry` command (#7790) * created Sentry setup command and templates * documented the setup sentry command * added opinionated defaults to Sentry Envelop plugin * relocated setup sentry handler to separate file * added note about SentryLayout useEffect dependency array * move cmd to `exp setup-sentry` --------- Co-authored-by: David Price --- .../src/commands/experimental/setupSentry.js | 23 ++ .../experimental/setupSentryHandler.js | 200 ++++++++++++++++++ .../templates/sentryApi.ts.template | 13 ++ .../templates/sentryWeb.ts.template | 11 + 4 files changed, 247 insertions(+) create mode 100644 packages/cli/src/commands/experimental/setupSentry.js create mode 100644 packages/cli/src/commands/experimental/setupSentryHandler.js create mode 100644 packages/cli/src/commands/experimental/templates/sentryApi.ts.template create mode 100644 packages/cli/src/commands/experimental/templates/sentryWeb.ts.template diff --git a/packages/cli/src/commands/experimental/setupSentry.js b/packages/cli/src/commands/experimental/setupSentry.js new file mode 100644 index 000000000000..001a7d096bb4 --- /dev/null +++ b/packages/cli/src/commands/experimental/setupSentry.js @@ -0,0 +1,23 @@ +import { getEpilogue } from './util' + +export const command = 'setup-sentry' + +export const description = 'Setup Sentry error and performance tracking' + +export const EXPERIMENTAL_TOPIC_ID = 4880 + +export const builder = (yargs) => { + yargs + .option('force', { + alias: 'f', + default: false, + description: 'Overwrite existing sentry.js config files', + type: 'boolean', + }) + .epilogue(getEpilogue(command, description, EXPERIMENTAL_TOPIC_ID, true)) +} + +export const handler = async (options) => { + const { handler } = await import('./setupSentryHandler') + return handler(options) +} diff --git a/packages/cli/src/commands/experimental/setupSentryHandler.js b/packages/cli/src/commands/experimental/setupSentryHandler.js new file mode 100644 index 000000000000..14a320ba6b0b --- /dev/null +++ b/packages/cli/src/commands/experimental/setupSentryHandler.js @@ -0,0 +1,200 @@ +import fs from 'fs' +import path from 'path' + +import { Listr } from 'listr2' + +import { + addApiPackages, + addEnvVarTask, + addWebPackages, + colors as c, + getPaths, + isTypeScriptProject, + prettify, + writeFilesTask, +} from '@redwoodjs/cli-helpers' +import { getConfigPath } from '@redwoodjs/project-config' +import { errorTelemetry } from '@redwoodjs/telemetry' + +import { writeFile } from '../../lib' + +const PATHS = getPaths() + +export const handler = async ({ force }) => { + const extension = isTypeScriptProject ? 'ts' : 'js' + + const notes = [] + + const tasks = new Listr([ + addApiPackages([ + '@envelop/sentry@5', + '@sentry/node@7', + '@sentry/tracing@7', + ]), + addWebPackages(['@sentry/react@7', '@sentry/tracing@7']), + addEnvVarTask( + 'SENTRY_DSN', + 'https://XXXXXXX@XXXXXXX.ingest.sentry.io/XXXXXXX', + 'https://docs.sentry.io/product/sentry-basics/dsn-explainer/' + ), + { + title: 'Setting up Sentry on the API and web sides', + task: () => + writeFilesTask( + { + [path.join(PATHS.api.lib, `sentry.${extension}`)]: fs + .readFileSync( + path.join(__dirname, 'templates/sentryApi.ts.template') + ) + .toString(), + [path.join(PATHS.web.src, 'lib', `sentry.${extension}`)]: fs + .readFileSync( + path.join(__dirname, 'templates/sentryWeb.ts.template') + ) + .toString(), + }, + { existingFiles: force ? 'OVERWRITE' : 'SKIP' } + ), + }, + { + title: 'Implementing the Envelop plugin', + task: (ctx) => { + const graphqlHandlerPath = path.join( + PATHS.api.functions, + `graphql.${extension}` + ) + + const contentLines = fs + .readFileSync(graphqlHandlerPath) + .toString() + .split('\n') + + const handlerIndex = contentLines.findLastIndex((line) => + /^export const handler = createGraphQLHandler\({/.test(line) + ) + + const pluginsIndex = contentLines.findLastIndex((line) => + /extraPlugins:/.test(line) + ) + + if (handlerIndex === -1 || pluginsIndex !== -1) { + ctx.addEnvelopPluginSkipped = true + return + } + + contentLines.splice( + handlerIndex, + 1, + "import 'src/lib/sentry'", + '', + 'export const handler = createGraphQLHandler({', + 'extraPlugins: [useSentry({', + 'includeRawResult: true,', + 'includeResolverArgs: true,', + 'includeExecuteVariables: true,', + '})],' + ) + + contentLines.splice(0, 0, "import { useSentry } from '@envelop/sentry'") + + fs.writeFileSync( + graphqlHandlerPath, + prettify('graphql.ts', contentLines.join('\n')) + ) + }, + }, + { + title: "Replacing Redwood's Error boundary", + task: () => { + const contentLines = fs + .readFileSync(PATHS.web.app) + .toString() + .split('\n') + + const webImportIndex = contentLines.findLastIndex((line) => + /^import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs\/web'$/.test( + line + ) + ) + contentLines.splice( + webImportIndex, + 1, + "import { RedwoodProvider } from '@redwoodjs/web'" + ) + + const boundaryOpenIndex = contentLines.findLastIndex((line) => + //.test(line) + ) + contentLines.splice( + boundaryOpenIndex, + 1, + '' + ) + + const boundaryCloseIndex = contentLines.findLastIndex((line) => + /<\/FatalErrorBoundary>/.test(line) + ) + contentLines.splice(boundaryCloseIndex, 1, '') + + contentLines.splice(0, 0, "import Sentry from 'src/lib/sentry'") + + fs.writeFileSync( + PATHS.web.app, + prettify('App.tsx', contentLines.join('\n')) + ) + }, + }, + { + title: 'Adding config to redwood.toml...', + task: (_ctx, task) => { + const redwoodTomlPath = getConfigPath() + const configContent = fs.readFileSync(redwoodTomlPath, 'utf-8') + if (!configContent.includes('[experimental.sentry]')) { + // Use string replace to preserve comments and formatting + writeFile( + redwoodTomlPath, + configContent.concat(`\n[experimental.sentry]\n\tenabled = true\n`), + { + overwriteExisting: true, // redwood.toml always exists + } + ) + } else { + task.skip( + `The [experimental.sentry] config block already exists in your 'redwood.toml' file.` + ) + } + }, + }, + { + title: 'One more thing...', + task: (ctx) => { + notes.push( + c.green( + 'You will need to add `SENTRY_DSN` to `includeEnvironmentVariables` in redwood.toml.' + ) + ) + + if (ctx.addEnvelopPluginSkipped) { + notes.push( + `${c.underline( + 'Make sure you implement the Sentry Envelop plugin:' + )} https://redwoodjs.com/docs/cli-commands#sentry-envelop-plugin` + ) + } else { + notes.push( + "Check out RedwoodJS' docs for more: https://redwoodjs.com/docs/cli-commands#setup-sentry" + ) + } + }, + }, + ]) + + try { + await tasks.run() + console.log(notes.join('\n')) + } catch (e) { + errorTelemetry(process.argv, e.message) + console.error(c.error(e.message)) + process.exit(e?.exitCode || 1) + } +} diff --git a/packages/cli/src/commands/experimental/templates/sentryApi.ts.template b/packages/cli/src/commands/experimental/templates/sentryApi.ts.template new file mode 100644 index 000000000000..29565f4977c6 --- /dev/null +++ b/packages/cli/src/commands/experimental/templates/sentryApi.ts.template @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node' +import * as Tracing from '@sentry/tracing' + +import { db as client } from 'src/lib/db' + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + environment: process.env.NODE_ENV, + integrations: [new Tracing.Integrations.Prisma({ client })], + tracesSampleRate: 1.0, +}) + +export default Sentry diff --git a/packages/cli/src/commands/experimental/templates/sentryWeb.ts.template b/packages/cli/src/commands/experimental/templates/sentryWeb.ts.template new file mode 100644 index 000000000000..4441183e29fb --- /dev/null +++ b/packages/cli/src/commands/experimental/templates/sentryWeb.ts.template @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/react' +import { BrowserTracing } from '@sentry/tracing' + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + environment: process.env.NODE_ENV, + integrations: [new BrowserTracing()], + tracesSampleRate: 1.0, +}) + +export default Sentry