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

86byre0hx: Implemented sentry generator #23

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions plugin/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 11 additions & 4 deletions plugin/src/generators/expo-app/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
);
}

Expand Down Expand Up @@ -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',
});
}
};
}

Expand Down
7 changes: 6 additions & 1 deletion plugin/src/generators/expo-app/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
5 changes: 5 additions & 0 deletions plugin/src/generators/next-app/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export async function nextAppGenerator(

return () => {
installPackagesTask(tree);
if (options.withSentry) {
execSync(`npx nx g sentry --directory=${options.directory}`, {
stdio: 'inherit',
});
}
};
}

Expand Down
7 changes: 6 additions & 1 deletion plugin/src/generators/next-app/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Original file line number Diff line number Diff line change
@@ -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 (
<html>
<body>
<NextError statusCode={undefined as any} />
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -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'],
});
12 changes: 12 additions & 0 deletions plugin/src/generators/sentry/files/sentry.edge.config.ts.template
Original file line number Diff line number Diff line change
@@ -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']
});
Original file line number Diff line number Diff line change
@@ -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'],
});
137 changes: 137 additions & 0 deletions plugin/src/generators/sentry/generator.ts
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

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

@rbd2q Unfortunately, the method of replacing anchor strings is unreliable and will eventually fail without warning. This can happen if someone changes the anchors, e.g. by adding or rearranging imports.

Can we instead work directly with the Abstract Syntax Tree (AST)? This would make the generator more stable and less reliant on specific anchors.

For more details and an example, see: https://nx.dev/extending-nx/recipes/modifying-files#ast-manipulation.

/^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;
3 changes: 3 additions & 0 deletions plugin/src/generators/sentry/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SentryGeneratorSchema {
name: string;
}
23 changes: 23 additions & 0 deletions plugin/src/generators/sentry/schema.json
Original file line number Diff line number Diff line change
@@ -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"]
}
2 changes: 2 additions & 0 deletions plugin/src/shared/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './format-utils';
export * from './cli-utils';
export * from './is-next-app';
export * from './is-expo-app'
5 changes: 5 additions & 0 deletions plugin/src/shared/utils/is-expo-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Tree } from '@nx/devkit';

export const isExpoApp = (tree: Tree, projectRoot: string): boolean => {
return tree.exists(`${projectRoot}/metro.config.js`);
};
5 changes: 5 additions & 0 deletions plugin/src/shared/utils/is-next-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Tree } from '@nx/devkit';

export const isNextApp = (tree: Tree, projectRoot: string): boolean => {
return tree.exists(`${projectRoot}/next.config.js`);
};