Skip to content
Merged
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 .changeset/nine-sloths-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'web-app': minor
---

Example with sentry
5 changes: 3 additions & 2 deletions apps/web-app/.env
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
# @see https://www.prisma.io/docs/concepts/components/prisma-client/deployment#recommended-connection-limit
PRISMA_DATABASE_URL=postgresql://nextjs:!ChangeMe!@localhost:5432/maindb?schema=public

# Enable display of lambdas size for vercel deployments
# NEXT_DEBUG_FUNCTION_SIZE=1
SENTRY_ENABLED=false
SENTRY_RELEASE=
NEXT_PUBLIC_SENTRY_DSN=
2 changes: 1 addition & 1 deletion apps/web-app/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const limitCfg = {
defaultSize: '100kb',
pages: {
'/_app': '120kb',
'/_error': '80kb',
'/_error': '100kb',
'/': '95kb',
'/demo': '95kb',
},
Expand Down
134 changes: 79 additions & 55 deletions apps/web-app/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const path = require('path');
const packageJson = require('./package');
const { withSentryConfig } = require('@sentry/nextjs');
const { i18n } = require('./next-i18next.config');
const NEXTJS_BUILD_TARGET = process.env.NEXTJS_BUILD_TARGET || 'server';
const NEXTJS_IGNORE_ESLINT = process.env.NEXTJS_IGNORE_ESLINT === '1' || false;
Expand Down Expand Up @@ -38,10 +40,6 @@ if (disableSourceMaps) {
);
}

const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});

// Example of setting up secure headers
// @link https://github.com/jagaapple/next-secure-headers
const { createSecureHeaders } = require('next-secure-headers');
Expand All @@ -63,64 +61,90 @@ const secureHeaders = createSecureHeaders({
referrerPolicy: 'same-origin',
});

const config = withBundleAnalyzer(
withTM({
target: NEXTJS_BUILD_TARGET,
reactStrictMode: true,
webpack5: true,
productionBrowserSourceMaps: !disableSourceMaps,
i18n,
optimizeFonts: true,
const baseConfig = withTM({
target: NEXTJS_BUILD_TARGET,
reactStrictMode: true,
webpack5: true,
productionBrowserSourceMaps: !disableSourceMaps,
i18n,
optimizeFonts: true,

// @link https://nextjs.org/docs/basic-features/image-optimization
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
disableStaticImages: false,
// Allowed domains for next/image
domains: ['source.unsplash.com'],
},
// @link https://nextjs.org/docs/basic-features/image-optimization
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
disableStaticImages: false,
// Allowed domains for next/image
domains: ['source.unsplash.com'],
},

eslint: {
ignoreDuringBuilds: NEXTJS_IGNORE_ESLINT,
dirs: ['src'],
},
eslint: {
ignoreDuringBuilds: NEXTJS_IGNORE_ESLINT,
dirs: ['src'],
},

async headers() {
return [{ source: '/(.*)', headers: secureHeaders }];
},
async headers() {
return [{ source: '/(.*)', headers: secureHeaders }];
},

webpack: (config, { defaultLoaders, isServer }) => {
// This extra config allows to use paths defined in tsconfig
// rather than next-transpile-modules.
// @link https://github.com/vercel/next.js/pull/13542
const resolvedBaseUrl = path.resolve(config.context, '../../');
config.module.rules = [
...config.module.rules,
{
test: /\.(tsx|ts|js|jsx|json)$/,
include: [resolvedBaseUrl],
use: defaultLoaders.babel,
exclude: (excludePath) => {
return /node_modules/.test(excludePath);
},
webpack: (config, { defaultLoaders, isServer }) => {
// This extra config allows to use paths defined in tsconfig
// rather than next-transpile-modules.
// @link https://github.com/vercel/next.js/pull/13542
const resolvedBaseUrl = path.resolve(config.context, '../../');
config.module.rules = [
...config.module.rules,
{
test: /\.(tsx|ts|js|jsx|json)$/,
include: [resolvedBaseUrl],
use: defaultLoaders.babel,
exclude: (excludePath) => {
return /node_modules/.test(excludePath);
},
];
},
];

// A temp workaround for https://github.com/prisma/prisma/issues/6899#issuecomment-849126557
if (isServer) {
config.externals.push('_http_common');
}
// A temp workaround for https://github.com/prisma/prisma/issues/6899#issuecomment-849126557
if (isServer) {
config.externals.push('_http_common');
}

config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts)x?$/,
use: ['@svgr/webpack'],
});
config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts)x?$/,
use: ['@svgr/webpack'],
});

return config;
},
})
);
return config;
},
env: {
APP_NAME: packageJson.name,
APP_VERSION: packageJson.version,
BUILD_TIME: new Date().getTime(),
SENTRY_RELEASE: process.env.SENTRY_RELEASE
? process.env.SENTRY_RELEASE
: `${packageJson.name}@${packageJson.version}`,
NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN,
},
serverRuntimeConfig: {
// to bypass https://github.com/zeit/next.js/issues/8251
PROJECT_ROOT: __dirname,
},
});

let config = baseConfig;

if (process.env.SENTRY_ENABLED === 'true') {
config = withSentryConfig(baseConfig, {
dryRun: process.env.NODE_ENV !== 'production',
});
}

if (process.env.ANALYZE === 'true') {
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: true,
});
config = withBundleAnalyzer(config);
}

module.exports = config;
1 change: 1 addition & 0 deletions apps/web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"@emotion/styled": "11.3.0",
"@headlessui/react": "1.4.0",
"@heroicons/react": "1.0.3",
"@sentry/nextjs": "^6.10.0",
"@tsed/exceptions": "6.62.0",
"@your-org/core-lib": "workspace:*",
"@your-org/db-main-prisma": "workspace:*",
Expand Down
11 changes: 11 additions & 0 deletions apps/web-app/sentry.client.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/nextjs';

const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;

Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
10 changes: 10 additions & 0 deletions apps/web-app/sentry.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# your base url
defaults.url=
# your org
defaults.org=
# your project
defaults.project=
# your auth token
auth.token=
# [optional] path to the cli executable
cli.executable=
13 changes: 13 additions & 0 deletions apps/web-app/sentry.server.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/nextjs';

const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;

Sentry.init({
dsn: SENTRY_DSN,
// for finer control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
1 change: 1 addition & 0 deletions apps/web-app/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type AppProps = NextAppProps & {
const MyApp = ({ Component, pageProps, err }: AppProps) => {
return (
<AppProviders>
{/* Workaround for https://github.com/vercel/next.js/issues/8592 */}
<Component {...pageProps} err={err} />
</AppProviders>
);
Expand Down
111 changes: 111 additions & 0 deletions apps/web-app/src/pages/_error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Typescript class based component for custom-error
* @link https://nextjs.org/docs/advanced-features/custom-error-page
*/

import NextErrorComponent from 'next/error';
import NextError, { ErrorProps } from 'next/error';
import { NextPage, NextPageContext } from 'next';

import {
captureException as sentryCaptureException,
flush as sentryFlush,
} from '@sentry/nextjs';
import { isNonEmptyString } from '@your-org/core-lib';

// Adds HttpException to the list of possible error types.
type AugmentedError = NonNullable<NextPageContext['err']> | null;
type CustomErrorProps = {
err?: AugmentedError;
message?: string;
sentryErrorId?: string;
hasGetInitialPropsRun?: boolean;
} & Omit<ErrorProps, 'err'>;

type AugmentedNextPageContext = Omit<NextPageContext, 'err'> & {
err: AugmentedError;
};

const captureException = async (err: string | Error) => {
if (isNonEmptyString(process.env.NEXT_PUBLIC_SENTRY_DSN)) {
sentryCaptureException(err);
}
};

const captureExceptionAndFlush = async (
err: string | Error,
flushAfter = 2000
) => {
if (isNonEmptyString(process.env.NEXT_PUBLIC_SENTRY_DSN)) {
sentryCaptureException(err);
if (flushAfter > 0) {
// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await sentryFlush(flushAfter);
}
}
};

const CustomError: NextPage<CustomErrorProps> = (props) => {
const { statusCode, err, hasGetInitialPropsRun } = props;

if (!hasGetInitialPropsRun && err) {
// getInitialProps is not called in case of
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
// err via _app.js so it can be captured
// Flushing is not required in this case as it only happens on the client
captureException(err);
}

return <NextErrorComponent statusCode={statusCode} />;
};

CustomError.getInitialProps = async ({
res,
err,
asPath,
}: AugmentedNextPageContext) => {
const errorInitialProps = (await NextError.getInitialProps({
res,
err,
} as NextPageContext)) as CustomErrorProps;

// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
errorInitialProps.hasGetInitialPropsRun = true;

// Running on the server, the response object (`res`) is available.
//
// Next.js will pass an err on the server if a page's data fetching methods
// threw or returned a Promise that rejected
//
// Running on the client (browser), Next.js will provide an err if:
//
// - a page's `getInitialProps` threw or returned a Promise that rejected
// - an exception was thrown somewhere in the React lifecycle (render,
// componentDidMount, etc) that was caught by Next.js's React Error
// Boundary. Read more about what types of exceptions are caught by Error
// Boundaries: https://reactjs.org/docs/error-boundaries.html

if (err) {
// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await captureExceptionAndFlush(err, 2000);
return errorInitialProps;
}

// If this point is reached, getInitialProps was called without any
// information about what the error might be. This is unexpected and may
// indicate a bug introduced in Next.js, so record it in Sentry
//
// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await captureExceptionAndFlush(
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
2000
);

return errorInitialProps;
};

export default CustomError;
2 changes: 1 addition & 1 deletion packages/core-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { Asserts } from './utils/asserts';
export { ArrayUtils } from './utils/array-utils';
export { RandomUtils } from './utils/random-utils';

export * as Typeguards from './utils/typeguards';
export * from './utils/typeguards';
export type { UnPromisify } from './utils/type-utils';

export const sayHello = (name: string): string => {
Expand Down
Loading