diff --git a/plugin/generators.json b/plugin/generators.json index 2d88f84..cd61257 100644 --- a/plugin/generators.json +++ b/plugin/generators.json @@ -30,6 +30,11 @@ "schema": "./src/generators/react-component/schema.json", "description": "react-component generator" }, + "sentry": { + "factory": "./src/generators/sentry/generator", + "schema": "./src/generators/sentry/schema.json", + "description": "sentry generator" + }, "ui-kitten": { "factory": "./src/shared/generators/ui-kitten/generator", "schema": "./src/shared/generators/ui-kitten/schema.json", diff --git a/plugin/src/generators/expo-app/generator.ts b/plugin/src/generators/expo-app/generator.ts index ef3adec..f6b679f 100644 --- a/plugin/src/generators/expo-app/generator.ts +++ b/plugin/src/generators/expo-app/generator.ts @@ -19,19 +19,19 @@ import { formatName, formatAppIdentifier } from '../../shared/utils'; export async function expoAppGenerator( tree: Tree, - options: ExpoAppGeneratorSchema + options: ExpoAppGeneratorSchema, ) { const appRoot = `apps/${options.directory}`; const appTestFolder = `apps/${options.directory}-e2e`; const libPath = `@${options.name}/${options.directory}`; // Install @nx/expo plugin - execSync('npx nx add @nx/expo', { stdio: 'inherit' }) + execSync('npx nx add @nx/expo', { stdio: 'inherit' }); if (!existsSync(appRoot)) { execSync( `npx nx g @nx/expo:app ${options.name} --directory=apps/${options.directory} --projectNameAndRootFormat=as-provided --unitTestRunner=none --e2eTestRunner=none`, - { stdio: 'inherit' } + { stdio: 'inherit' }, ); } @@ -100,7 +100,14 @@ export async function expoAppGenerator( return () => { installPackagesTask(tree); execSync('npx expo install --fix', { stdio: 'inherit' }); - execSync(`npx nx g ui-kitten ${options.name} ${options.directory}`, { stdio: 'inherit' }); + execSync(`npx nx g ui-kitten ${options.name} ${options.directory}`, { + stdio: 'inherit', + }); + if (options.withSentry) { + execSync(`npx nx g sentry --directory=${options.directory}`, { + stdio: 'inherit', + }); + } }; } diff --git a/plugin/src/generators/expo-app/schema.json b/plugin/src/generators/expo-app/schema.json index 4b4c67f..e39544a 100644 --- a/plugin/src/generators/expo-app/schema.json +++ b/plugin/src/generators/expo-app/schema.json @@ -21,7 +21,12 @@ "index": 1 }, "x-prompt": "Enter the name of the directory in the 'apps/' folder (e.g: mobile)" + }, + "withSentry": { + "type": "boolean", + "default": false, + "x-prompt": "Do you want to use sentry in your app?" } }, - "required": ["name", "directory"] + "required": ["name", "directory", "withSentry"] } diff --git a/plugin/src/generators/next-app/generator.ts b/plugin/src/generators/next-app/generator.ts index da9af87..1e21698 100644 --- a/plugin/src/generators/next-app/generator.ts +++ b/plugin/src/generators/next-app/generator.ts @@ -69,6 +69,11 @@ export async function nextAppGenerator( return () => { installPackagesTask(tree); + if (options.withSentry) { + execSync(`npx nx g sentry --directory=${options.directory}`, { + stdio: 'inherit', + }); + } }; } diff --git a/plugin/src/generators/next-app/schema.json b/plugin/src/generators/next-app/schema.json index a385502..85bc36a 100644 --- a/plugin/src/generators/next-app/schema.json +++ b/plugin/src/generators/next-app/schema.json @@ -21,7 +21,12 @@ "index": 1 }, "x-prompt": "Enter the name of the directory in the 'apps/' folder (e.g: web)" + }, + "withSentry": { + "type": "boolean", + "default": false, + "x-prompt": "Do you want to use sentry in your app?" } }, - "required": ["name", "directory"] + "required": ["name", "directory", "withSentry"] } diff --git a/plugin/src/generators/sentry/files/app/[locale]/global-error.tsx.template b/plugin/src/generators/sentry/files/app/[locale]/global-error.tsx.template new file mode 100644 index 0000000..fbd1c3d --- /dev/null +++ b/plugin/src/generators/sentry/files/app/[locale]/global-error.tsx.template @@ -0,0 +1,23 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { ReactElement, useEffect } from 'react'; + +interface GlobalErrorProps { + error: Error & { digest?: string }; +} + +export default function GlobalError({ error }: GlobalErrorProps): ReactElement { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/plugin/src/generators/sentry/files/sentry.client.config.ts.template b/plugin/src/generators/sentry/files/sentry.client.config.ts.template new file mode 100644 index 0000000..cd3410d --- /dev/null +++ b/plugin/src/generators/sentry/files/sentry.client.config.ts.template @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: '<%= DSN %>', + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + debug: false, + replaysOnErrorSampleRate: 1.0, + + // 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, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + Sentry.replayIntegration(), + Sentry.inboundFiltersIntegration(), + ], + denyUrls: ['localhost'], +}); diff --git a/plugin/src/generators/sentry/files/sentry.edge.config.ts.template b/plugin/src/generators/sentry/files/sentry.edge.config.ts.template new file mode 100644 index 0000000..bba4747 --- /dev/null +++ b/plugin/src/generators/sentry/files/sentry.edge.config.ts.template @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: '<%= DSN %>', + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + debug: false, + integrations: [Sentry.inboundFiltersIntegration()], + denyUrls: ['localhost'] +}); diff --git a/plugin/src/generators/sentry/files/sentry.server.config.ts.template b/plugin/src/generators/sentry/files/sentry.server.config.ts.template new file mode 100644 index 0000000..ded1139 --- /dev/null +++ b/plugin/src/generators/sentry/files/sentry.server.config.ts.template @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: '<%= DSN %>', + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + debug: false, + integrations: [Sentry.inboundFiltersIntegration()], + denyUrls: ['localhost'], +}); diff --git a/plugin/src/generators/sentry/generator.ts b/plugin/src/generators/sentry/generator.ts new file mode 100644 index 0000000..db59adb --- /dev/null +++ b/plugin/src/generators/sentry/generator.ts @@ -0,0 +1,137 @@ +import { + addDependenciesToPackageJson, + formatFiles, + generateFiles, + Tree, +} from '@nx/devkit'; +import * as path from 'path'; +import { SentryGeneratorSchema } from './schema'; +import { isExpoApp, isNextApp } from '../../shared/utils'; + +const nextAppDependencies = { + '@sentry/nextjs': '^8.21.0', +}; + +const expoAppDependencies = { + '@sentry/react-native': '~5.22.0', +}; + +export async function sentryGenerator( + tree: Tree, + options: SentryGeneratorSchema, +) { + const projectRoot = `apps/${options.directory}`; + + if (isNextApp(tree, projectRoot)) { + addDependenciesToPackageJson(tree, nextAppDependencies, {}); + + const nextConfigContent = tree + .read(`${projectRoot}/next.config.js`) + .toString() + .replace( + /^const { withNx } = require\('@nrwl\/next\/plugins\/with-nx'\);$/gm, + `const { withSentryConfig } = require("@sentry/nextjs"); + const { withNx } = require('@nrwl/next/plugins/with-nx');`, + ) + .replace( + /^const nextConfig = {/gm, + `const nextConfig = { + sentry: { + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Transpiles SDK to be compatible with IE11 (increases bundle size) + transpileClientSDK: true, + + // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) + tunnelRoute: '/monitoring', + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true + }, + `, + ) + .replace( + /\(nextConfig\)/gm, + `(withSentryConfig(nextConfig, sentryWebpackPluginOptions))`, + ); + + tree.write( + `${projectRoot}/next.config.js`, + nextConfigContent + + ` + /** + * @type {import('@sentry/nextjs').SentryWebpackPluginOptions} + **/ + + const sentryWebpackPluginOptions = { + silent: true, + org: '', + project: 'web-next-js-client', + authToken: process.env.SENTRY_AUTH_TOKEN, + }; + + `, + ); + + const envFiles = ['.env', '.env.development', '.env.production']; + envFiles.forEach((file) => { + const envContent = tree.read(`${projectRoot}/${file}`).toString(); + tree.write(`${projectRoot}/${file}`, envContent + 'SENTRY_AUTH_TOKEN='); + }); + + generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options); + } else if (isExpoApp(tree, projectRoot)) { + addDependenciesToPackageJson(tree, expoAppDependencies, {}); + + const layoutContent = tree + .read(`${projectRoot}/app/_layout.tsx`) + .toString() + .replace( + /^import { Stack } from 'expo-router';$/gm, + `import { Stack } from 'expo-router';import * as Sentry from "@sentry/react-native";import Constants from "expo-constants";`, + ) + .replace(/^export default function RootLayout/gm, `function RootLayout`); + + tree.write( + `${projectRoot}/app/_layout.tsx`, + layoutContent + + ` + const routingInstrumentation = new Sentry.ReactNavigationInstrumentation(); + + Sentry.init({ + dsn: Constants.expoConfig?.extra?.sentry?.dsn, + environment: Constants.expoConfig?.extra?.env, + debug: false, + integrations: [new Sentry.ReactNativeTracing({ routingInstrumentation })], + enabled: !__DEV__ + }); + + export default Sentry.wrap(RootLayout);`, + ); + + const appConfigContent = tree + .read(`${projectRoot}/app.config.ts`) + .toString() + .replace( + /plugins: \[/g, + `plugins: [ [ + '@sentry/react-native/expo', + { + // TODO Update organization and project name + organization: '', + project: '' + } + ],`, + ); + + tree.write(`${projectRoot}/app.config.ts`, appConfigContent); + } + + await formatFiles(tree); +} + +export default sentryGenerator; diff --git a/plugin/src/generators/sentry/schema.d.ts b/plugin/src/generators/sentry/schema.d.ts new file mode 100644 index 0000000..5ef0af3 --- /dev/null +++ b/plugin/src/generators/sentry/schema.d.ts @@ -0,0 +1,3 @@ +export interface SentryGeneratorSchema { + name: string; +} diff --git a/plugin/src/generators/sentry/schema.json b/plugin/src/generators/sentry/schema.json new file mode 100644 index 0000000..070ac35 --- /dev/null +++ b/plugin/src/generators/sentry/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "Sentry", + "title": "", + "type": "object", + "properties": { + "directory": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "Enter the name of the directory in the 'apps/' folder (e.g: web)" + }, + "DSN": { + "type": "string", + "description": "", + "x-prompt": "What is your Sentry DSN?" + } + }, + "required": ["directory", "DSN"] +} diff --git a/plugin/src/shared/utils/index.ts b/plugin/src/shared/utils/index.ts index 9125b36..e326a9f 100644 --- a/plugin/src/shared/utils/index.ts +++ b/plugin/src/shared/utils/index.ts @@ -1,2 +1,4 @@ export * from './format-utils'; export * from './cli-utils'; +export * from './is-next-app'; +export * from './is-expo-app' diff --git a/plugin/src/shared/utils/is-expo-app.ts b/plugin/src/shared/utils/is-expo-app.ts new file mode 100644 index 0000000..bd4e16a --- /dev/null +++ b/plugin/src/shared/utils/is-expo-app.ts @@ -0,0 +1,5 @@ +import { Tree } from '@nx/devkit'; + +export const isExpoApp = (tree: Tree, projectRoot: string): boolean => { + return tree.exists(`${projectRoot}/metro.config.js`); +}; diff --git a/plugin/src/shared/utils/is-next-app.ts b/plugin/src/shared/utils/is-next-app.ts new file mode 100644 index 0000000..f3ee77e --- /dev/null +++ b/plugin/src/shared/utils/is-next-app.ts @@ -0,0 +1,5 @@ +import { Tree } from '@nx/devkit'; + +export const isNextApp = (tree: Tree, projectRoot: string): boolean => { + return tree.exists(`${projectRoot}/next.config.js`); +};