Skip to content
Closed
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
6 changes: 6 additions & 0 deletions packages/next-plugin-sentry/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* TODO(kamil): Unify SDK configuration options.
* Workaround for callbacks, integrations and any unserializable config options.
**/
exports.serverConfig = {}
Copy link
Member

Choose a reason for hiding this comment

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

The runtime configuration approach is not needed if you compile the configuration values into the bundle (prefixing using NEXT_PUBLIC_. However keep in mind that we shouldn't do that for the getRelease part given it will cause every single build to invalidate the webpack cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but there's no way to provide additional configuration to Sentry.init without that approach, as NEXT_PUBLIC_ supports only serializable values.

exports.clientConfig = {}
9 changes: 9 additions & 0 deletions packages/next-plugin-sentry/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const getDsn = () =>
process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN

export const getRelease = () =>
process.env.SENTRY_RELEASE ||
process.env.NEXT_PUBLIC_SENTRY_RELEASE ||
process.env.VERCEL_GITHUB_COMMIT_SHA ||
process.env.VERCEL_GITLAB_COMMIT_SHA ||
process.env.VERCEL_BITBUCKET_COMMIT_SHA
11 changes: 11 additions & 0 deletions packages/next-plugin-sentry/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { serverConfig, clientConfig } = require('./config.js')

const Sentry = require('@sentry/minimal')
// NOTE(kamiL): @sentry/minimal doesn't expose this method, as it's not env-agnostic, but we still want to test it here
Sentry.showReportDialog = (...args) => {
Sentry._callOnClient('showReportDialog', ...args)
}

exports.Sentry = Sentry
exports.serverConfig = serverConfig
exports.clientConfig = clientConfig
11 changes: 5 additions & 6 deletions packages/next-plugin-sentry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
},
"nextjs": {
"name": "Sentry",
"required-env": [
"SENTRY_DSN",
"SENTRY_RELEASE"
]
"required-env": []
Copy link
Member

Choose a reason for hiding this comment

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

Why are there no longer any required envs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See the PR description.

Copy link
Member

Choose a reason for hiding this comment

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

We should fix the bug with the key please instead of bypassing it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I totally agree. This PR is the effect of PoC work, and it was necessary to go around this limitation for demo purposes.

},
"peerDependencies": {
"next": "*"
},
"dependencies": {
"@sentry/browser": "5.7.1",
"@sentry/node": "5.7.1"
"@sentry/integrations": "5.27.0",
"@sentry/node": "5.27.0",
"@sentry/browser": "5.27.0",
"@sentry/minimal": "5.27.0"
}
}
99 changes: 97 additions & 2 deletions packages/next-plugin-sentry/readme.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,98 @@
# Unstable @next/plugin-sentry
# Next.js + Sentry

This package is still very experimental and should not be used at this point
Capture unhandled exceptions with Sentry in your Next.js project.

## Installation

```
npm install @next/plugin-sentry
```

or

```
yarn add @next/plugin-sentry
```

Provide necessary env variables through `.env` or available way

```
NEXT_PUBLIC_SENTRY_DSN
// variables below are required only when using with @next/sentry-source-maps
NEXT_PUBLIC_SENTRY_RELEASE
SENTRY_PROJECT
SENTRY_ORG
SENTRY_AUTH_TOKEN
```

### Usage

Create a next.config.js

```js
// next.config.js
module.exports = {
experimental: { plugins: true },
}
```

With only that, you'll get a complete error coverage for your application.
If you want to use Sentry SDK APIs, you can do so in both, server-side and client-side code with the same namespace from the plugin.

```js
import { Sentry } from '@next/plugin-sentry'

const MyComponent = () => <h1>Server Test 1</h1>

export function getServerSideProps() {
if (!this.veryImportantValue) {
Sentry.withScope((scope) => {
scope.setTag('method', 'getServerSideProps')
Sentry.captureMessage('veryImportantValue is missing')
})
}

return {}
}

export default MyComponent
```

### Configuration

There are two ways to configure Sentry SDK. One through `next.config.js` which allows for the full configuration of the server-side code, and partial configuration of client-side code. And additional method for client-side code.

```js
// next.config.js
module.exports = {
experimental: { plugins: true },
// Sentry.init config for server-side code. Can accept any available config option.
serverRuntimeConfig: {
sentry: {
type: 'server',
},
},
// Sentry.init config for client-side code (and fallback for server-side)
// can accept only serializeable values. For more granular control see below.
publicRuntimeConfig: {
sentry: {
type: 'client',
},
},
Comment on lines +71 to +82
Copy link
Member

Choose a reason for hiding this comment

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

This should be moved to your own sentry.config.js file or similar.

}
```

If you need to pass config options for the client-side, that are non-serializable, for example `beforeSend` or `beforeBreadcrumb`:

```js
// _app.js
import { clientConfig } from '@next/plugin-sentry'

clientConfig.beforeSend = () => {
/* ... */
}
clientConfig.beforeBreadcrumb = () => {
/* ... */
}
```
13 changes: 10 additions & 3 deletions packages/next-plugin-sentry/src/on-error-client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as Sentry from '@sentry/browser'
import { withScope, captureException } from '@sentry/browser'

export default async function onErrorClient({ err }) {
Sentry.captureException(err)
export default async function onErrorClient({ err, errorInfo }) {
withScope((scope) => {
if (typeof errorInfo?.componentStack === 'string') {
scope.setContext('react', {
componentStack: errorInfo.componentStack.trim(),
})
}
captureException(err)
})
}
28 changes: 25 additions & 3 deletions packages/next-plugin-sentry/src/on-error-server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import * as Sentry from '@sentry/node'
import { captureException, flush, Handlers, withScope } from '@sentry/node'
import getConfig from 'next/config'

export default async function onErrorServer({ err }) {
Sentry.captureException(err)
const { parseRequest } = Handlers

export default async function onErrorServer({ err, req }) {
const { serverRuntimeConfig = {}, publicRuntimeConfig = {} } =
getConfig() || {}
Comment on lines +7 to +8
Copy link
Member

Choose a reason for hiding this comment

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

next/config should not be used. This is not compatible with Next.js' SSG mode and only exists for legacy reasons.

const sentryTimeout =
serverRuntimeConfig.sentryTimeout ||
publicRuntimeConfig.sentryTimeout ||
2000

withScope((scope) => {
if (req) {
scope.addEventProcessor((event) =>
parseRequest(event, req, {
// TODO(kamil): 'cookies' and 'query_string' use `dynamicRequire` which has a bug in SSR envs right now.
request: ['data', 'headers', 'method', 'url'],
})
)
}
captureException(err)
})

await flush(sentryTimeout)
}
23 changes: 17 additions & 6 deletions packages/next-plugin-sentry/src/on-init-client.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import * as Sentry from '@sentry/browser'
import { init } from '@sentry/browser'
import getConfig from 'next/config'

import { getDsn, getRelease } from '../env'
import { clientConfig } from '../config'

export default async function initClient() {
// by default `@sentry/browser` is configured with defaultIntegrations
// which capture uncaughtExceptions and unhandledRejections
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: process.env.SENTRY_RELEASE,
/**
* TODO(kamil): Unify SDK configuration options.
* RuntimeConfig cannot be used for callbacks and integrations as it supports only serializable data.
**/
const { publicRuntimeConfig = {} } = getConfig() || {}
const runtimeConfig = publicRuntimeConfig.sentry || {}
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

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

next/config should not be used. This is not compatible with Next.js' SSG mode and only exists for legacy reasons.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Timer What do you suggest to replace it?


init({
dsn: getDsn(),
...(getRelease() && { release: getRelease() }),
...runtimeConfig,
...clientConfig,
})
}
42 changes: 35 additions & 7 deletions packages/next-plugin-sentry/src/on-init-server.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
import * as Sentry from '@sentry/node'
import { init } from '@sentry/node'
import { RewriteFrames } from '@sentry/integrations'
import getConfig from 'next/config'

import { getDsn, getRelease } from '../env'
import { serverConfig } from '../config'

export default async function initServer() {
// by default `@sentry/node` is configured with defaultIntegrations
// which capture uncaughtExceptions and unhandledRejections
// see here for more https://github.com/getsentry/sentry-javascript/blob/46a02209bafcbc1603c769476ba0a1eaa450759d/packages/node/src/sdk.ts#L22
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: process.env.SENTRY_RELEASE,
/**
* TODO(kamil): Unify SDK configuration options.
* RuntimeConfig cannot be used for callbacks and integrations as it supports only serializable data.
**/
const { serverRuntimeConfig = {}, publicRuntimeConfig = {} } =
getConfig() || {}
const runtimeConfig =
serverRuntimeConfig.sentry || publicRuntimeConfig.sentry || {}
Comment on lines +13 to +16
Copy link
Member

Choose a reason for hiding this comment

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

next/config should not be used. This is not compatible with Next.js' SSG mode and only exists for legacy reasons.


init({
dsn: getDsn(),
...(getRelease() && { release: getRelease() }),
...runtimeConfig,
...serverConfig,
integrations: [
new RewriteFrames({
iteratee: (frame) => {
try {
const [, path] = frame.filename.split('.next/')
if (path) {
frame.filename = `app:///_next/${path}`
}
} catch {}
return frame
},
}),
...(runtimeConfig.integrations || []),
...(serverConfig.integrations || []),
],
})
}
4 changes: 2 additions & 2 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ const nextServerlessLoader: loader.Loader = function () {
)
} catch (err) {
console.error(err)
await onError(err)
await onError({ err, req, res })

// TODO: better error for DECODE_FAILED?
if (err.code === 'DECODE_FAILED') {
Expand Down Expand Up @@ -846,7 +846,7 @@ const nextServerlessLoader: loader.Loader = function () {
}
} catch(err) {
console.error(err)
await onError(err)
await onError({ err, req, res })
// Throw the error to crash the serverless function
throw err
}
Expand Down
27 changes: 19 additions & 8 deletions packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type Router from '../next-server/lib/router/router'
import type {
AppComponent,
AppProps,
ErrorInfo,
PrivateRouteInfo,
} from '../next-server/lib/router/router'
import { delBasePath, hasBasePath } from '../next-server/lib/router/router'
Expand Down Expand Up @@ -142,10 +143,10 @@ let cachedStyleSheets: StyleSheetTuple[]
let CachedApp: AppComponent, onPerfEntry: (metric: any) => void

class Container extends React.Component<{
fn: (err: Error, info?: any) => void
fn: (err: Error, errorInfo?: any) => void
}> {
componentDidCatch(componentErr: Error, info: any) {
this.props.fn(componentErr, info)
componentDidCatch(componentErr: Error, errorInfo: ErrorInfo) {
this.props.fn(componentErr, errorInfo)
}

componentDidMount() {
Expand Down Expand Up @@ -391,7 +392,7 @@ export async function render(renderingProps: RenderRouteInfo) {
// 404 and 500 errors are special kind of errors
// and they are still handle via the main render method.
export function renderError(renderErrorProps: RenderErrorProps) {
const { App, err } = renderErrorProps
const { App, err, errorInfo } = renderErrorProps

// In development runtime errors are caught by our overlay
// In production we catch runtime errors using componentDidCatch which will trigger renderError
Expand All @@ -409,12 +410,19 @@ export function renderError(renderErrorProps: RenderErrorProps) {
styleSheets: [],
})
}

if (process.env.__NEXT_PLUGINS) {
// @ts-ignore
// eslint-disable-next-line
import('next-plugin-loader?middleware=on-error-client!')
.then((onClientErrorModule) => {
return onClientErrorModule.default({ err })
return onClientErrorModule.default({
err,
errorInfo,
renderErrorProps,
data,
version,
})
})
.catch((onClientErrorErr) => {
console.error(
Expand All @@ -437,7 +445,7 @@ export function renderError(renderErrorProps: RenderErrorProps) {
Component: ErrorComponent,
AppTree,
router,
ctx: { err, pathname: page, query, asPath, AppTree },
ctx: { err, errorInfo, pathname: page, query, asPath, AppTree },
}
return Promise.resolve(
renderErrorProps.props
Expand All @@ -447,6 +455,7 @@ export function renderError(renderErrorProps: RenderErrorProps) {
doRender({
...renderErrorProps,
err,
errorInfo,
Component: ErrorComponent,
styleSheets,
props: initProps,
Expand Down Expand Up @@ -544,8 +553,8 @@ function AppContainer({
}: React.PropsWithChildren<{}>): React.ReactElement {
return (
<Container
fn={(error) =>
renderError({ App: CachedApp, err: error }).catch((err) =>
fn={(error, errorInfo) =>
renderError({ App: CachedApp, err: error, errorInfo }).catch((err) =>
console.error('Error rendering page: ', err)
)
}
Expand Down Expand Up @@ -580,6 +589,7 @@ function doRender({
Component,
props,
err,
errorInfo,
styleSheets,
}: RenderRouteInfo): Promise<any> {
Component = Component || lastAppProps.Component
Expand All @@ -589,6 +599,7 @@ function doRender({
...props,
Component,
err,
errorInfo,
router,
}
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
Expand Down
Loading