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`);
+};