feat(plugin-sentry): update plugin to 3.0 (#8613)
Updates the plugin to 3.0
Test:
```sh
NEXT_PUBLIC_SENTRY_DSN=<DSN here> pnpm dev plugin-sentry
```
Example:
```ts
sentryPlugin({
options: {
captureErrors: [400, 403],
context: ({ defaultContext, req }) => {
return {
...defaultContext,
tags: {
locale: req.locale,
},
}
},
debug: true,
},
Sentry,
})
```
This commit is contained in:
27
app/global-error.tsx
Normal file
27
app/global-error.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
'use client'
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import NextError from 'next/error.js'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function GlobalError({ error }: { error: { digest?: string } & Error }) {
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<html lang="en-US">
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
definition requires a `statusCode` prop. However, since the App Router
|
||||
does not expose status codes for errors, we simply pass 0 to render a
|
||||
generic error message. */}
|
||||
{/* @ts-expect-error types repo */}
|
||||
<NextError statusCode={0} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ This multi-faceted software offers a range of features that will help you manage
|
||||
- **Integrations**: Connects with various tools and services for enhanced workflow and issue management
|
||||
|
||||
<Banner type="info">
|
||||
This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-sentry). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20seo&template=bug_report.md&title=plugin-seo%3A) with as much detail as possible.
|
||||
This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/beta/packages/plugin-sentry). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20seo&template=bug_report.md&title=plugin-sentry%3A) with as much detail as possible.
|
||||
</Banner>
|
||||
|
||||
## Installation
|
||||
@@ -42,6 +42,15 @@ Install the plugin using any JavaScript package manager like [Yarn](https://yarn
|
||||
pnpm add @payloadcms/plugin-sentry
|
||||
```
|
||||
|
||||
## Sentry for Next.js setup
|
||||
This plugin requires to complete the [Sentry + Next.js setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/) before.
|
||||
|
||||
You can use either the [automatic setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/#install) with the installation wizard:
|
||||
```sh
|
||||
npx @sentry/wizard@latest -i nextjs
|
||||
```
|
||||
Or the [Manual Setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
In the `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin and pass in your Sentry DSN as an option.
|
||||
@@ -51,11 +60,13 @@ import { buildConfig } from 'payload'
|
||||
import { sentryPlugin } from '@payloadcms/plugin-sentry'
|
||||
import { Pages, Media } from './collections'
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [Pages, Media],
|
||||
plugins: [
|
||||
sentryPlugin({
|
||||
dsn: 'https://61edebas776889984d323d777@o4505289711681536.ingest.sentry.io/4505357433352176',
|
||||
Sentry,
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -65,58 +76,55 @@ export default config
|
||||
|
||||
## Options
|
||||
|
||||
- `dsn` : string | **required**
|
||||
- `Sentry` : Sentry | **required**
|
||||
|
||||
Sentry automatically assigns a DSN when you create a project, the unique DSN informs Sentry where to send events so they are associated with the correct project.
|
||||
The `Sentry` instance
|
||||
|
||||
<Banner type="warning">
|
||||
You can find your project DSN (Data Source Name) by visiting [sentry.io](sentry.io) and navigating to your [Project] > Settings > Client Keys (DSN).
|
||||
Make sure to complete the [Sentry for Next.js Setup](#sentry-for-nextjs-setup) before.
|
||||
</Banner>
|
||||
|
||||
- `enabled`: boolean | optional
|
||||
|
||||
Set to false to disable the plugin. Defaults to true.
|
||||
Set to false to disable the plugin. Defaults to `true`.
|
||||
|
||||
- `init` : ClientOptions | optional
|
||||
- `context`: `(args: ContextArgs) => Partial<ScopeContext> | Promise<Partial<ScopeContext>>`
|
||||
|
||||
Sentry allows a variety of options to be passed into the Sentry.init() function, see the full list of options [here](https://docs.sentry.io/platforms/node/guides/express/configuration/options).
|
||||
|
||||
- `requestHandler` : RequestHandlerOptions | optional
|
||||
|
||||
Accepts options that let you decide what data should be included in the event sent to Sentry, checkout the options [here](https://docs.sentry.io/platforms/node/guides/express/configuration/options).
|
||||
Pass additional [contextual data](https://docs.sentry.io/platforms/javascript/enriching-events/context/#passing-context-directly) to Sentry
|
||||
|
||||
- `captureErrors`: number[] | optional
|
||||
|
||||
By default, `Sentry.errorHandler` will capture only errors with a status code of 500 or higher. To capture additional error codes, pass the values as numbers in an array.
|
||||
|
||||
To see all options available, visit the [Sentry Docs](https://docs.sentry.io/platforms/node/guides/express/configuration/options).
|
||||
|
||||
### Example
|
||||
|
||||
Configure any of these options by passing them to the plugin:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { sentry } from '@payloadcms/plugin-sentry'
|
||||
import { sentryPlugin } from '@payloadcms/plugin-sentry'
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
import { Pages, Media } from './collections'
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [Pages, Media],
|
||||
plugins: [
|
||||
sentry({
|
||||
dsn: 'https://61edebas777689984d323d777@o4505289711681536.ingest.sentry.io/4505357433352176',
|
||||
sentryPlugin({
|
||||
options: {
|
||||
init: {
|
||||
captureErrors: [400, 403],
|
||||
context: ({ defaultContext, req }) => {
|
||||
return {
|
||||
...defaultContext,
|
||||
tags: {
|
||||
locale: req.locale,
|
||||
},
|
||||
}
|
||||
},
|
||||
debug: true,
|
||||
environment: 'development',
|
||||
tracesSampleRate: 1.0,
|
||||
},
|
||||
requestHandler: {
|
||||
serverName: false,
|
||||
user: ['email'],
|
||||
},
|
||||
captureErrors: [400, 403, 404],
|
||||
},
|
||||
Sentry,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
5
instrumentation.ts
Normal file
5
instrumentation.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config.js')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import bundleAnalyzer from '@next/bundle-analyzer'
|
||||
|
||||
import withPayload from './packages/next/src/withPayload.js'
|
||||
import { withSentryConfig } from '@sentry/nextjs'
|
||||
import { withPayload } from './packages/next/src/withPayload.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -11,8 +11,7 @@ const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default withBundleAnalyzer(
|
||||
const config = withBundleAnalyzer(
|
||||
withPayload({
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
@@ -23,7 +22,6 @@ export default withBundleAnalyzer(
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
PAYLOAD_CI_DEPENDENCY_CHECKER: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
@@ -48,3 +46,8 @@ export default withBundleAnalyzer(
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default withSentryConfig(config, {
|
||||
telemetry: false,
|
||||
tunnelRoute: '/monitoring-tunnel',
|
||||
})
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
"@payloadcms/live-preview-react": "workspace:*",
|
||||
"@playwright/test": "1.46.0",
|
||||
"@sentry/nextjs": "^8.33.1",
|
||||
"@sentry/node": "^8.33.1",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"@swc/cli": "0.4.0",
|
||||
"@swc/jest": "0.2.36",
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -74,9 +71,5 @@
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"homepage:": "https://payloadcms.com",
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
}
|
||||
"homepage:": "https://payloadcms.com"
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "0.0.6",
|
||||
"version": "3.0.0-beta.111",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
@@ -23,6 +23,11 @@
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./src/exports/client.ts",
|
||||
"types": "./src/exports/client.ts",
|
||||
"default": "./src/exports/client.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
@@ -31,32 +36,24 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "echo \"Build temporarily disabled.\" && exit 0",
|
||||
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc-build --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^7.55.2",
|
||||
"@sentry/types": "^7.54.0",
|
||||
"express": "^4.18.2"
|
||||
"@sentry/nextjs": "^8.33.1",
|
||||
"@sentry/types": "^8.33.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "3.0.3",
|
||||
"payload": "workspace:*",
|
||||
"ts-jest": "^29.1.0"
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "workspace:*",
|
||||
@@ -69,6 +66,11 @@
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/exports/client.js",
|
||||
"types": "./dist/exports/client.d.ts",
|
||||
"default": "./dist/exports/client.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as Sentry from '@sentry/node'
|
||||
|
||||
export const captureException = (err: Error): void => {
|
||||
Sentry.captureException(err)
|
||||
}
|
||||
1
packages/plugin-sentry/src/exports/client.ts
Normal file
1
packages/plugin-sentry/src/exports/client.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AdminErrorBoundary } from '../providers/AdminErrorBoundary.js'
|
||||
@@ -1,2 +1,94 @@
|
||||
export { sentryPlugin } from './plugin.js'
|
||||
export type { PluginOptions } from './types.js'
|
||||
import type { ScopeContext } from '@sentry/types'
|
||||
import type { APIError, Config } from 'payload'
|
||||
|
||||
import type { PluginOptions } from './types.js'
|
||||
|
||||
export { PluginOptions }
|
||||
/**
|
||||
* @example
|
||||
* ```ts
|
||||
* import * as Sentry from '@sentry/nextjs'
|
||||
*
|
||||
* sentryPlugin({
|
||||
* options: {
|
||||
* captureErrors: [400, 403],
|
||||
* context: ({ defaultContext, req }) => {
|
||||
* return {
|
||||
* ...defaultContext,
|
||||
* tags: {
|
||||
* locale: req.locale,
|
||||
* },
|
||||
* }
|
||||
* },
|
||||
* debug: true,
|
||||
* },
|
||||
* Sentry,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const sentryPlugin =
|
||||
(pluginOptions: PluginOptions) =>
|
||||
(config: Config): Config => {
|
||||
const { enabled = true, options = {}, Sentry } = pluginOptions
|
||||
|
||||
if (!enabled || !Sentry) {
|
||||
return config
|
||||
}
|
||||
|
||||
const { captureErrors = [], debug = false } = options
|
||||
|
||||
return {
|
||||
...config,
|
||||
admin: {
|
||||
...config.admin,
|
||||
components: {
|
||||
...config.admin?.components,
|
||||
providers: [
|
||||
...(config.admin?.components?.providers ?? []),
|
||||
'@payloadcms/plugin-sentry/client#AdminErrorBoundary',
|
||||
],
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
afterError: [
|
||||
...(config.hooks?.afterError ?? []),
|
||||
async (args) => {
|
||||
if ('status' in args.error) {
|
||||
const apiError = args.error as APIError
|
||||
if (apiError.status >= 500 || captureErrors.includes(apiError.status)) {
|
||||
let context: Partial<ScopeContext> = {
|
||||
extra: {
|
||||
errorCollectionSlug: args.collection?.slug,
|
||||
},
|
||||
...(args.req.user && {
|
||||
user: {
|
||||
id: args.req.user.id,
|
||||
collection: args.req.user.collection,
|
||||
email: args.req.user.email,
|
||||
ip_address: args.req.headers?.get('X-Forwarded-For') ?? undefined,
|
||||
username: args.req.user.username,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
if (options?.context) {
|
||||
context = await options.context({
|
||||
...args,
|
||||
defaultContext: context,
|
||||
})
|
||||
}
|
||||
|
||||
const id = Sentry.captureException(args.error, context)
|
||||
|
||||
if (debug) {
|
||||
args.req.payload.logger.info(
|
||||
`Captured exception ${id} to Sentry, error msg: ${args.error.message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
captureException: () => {},
|
||||
startSentry: () => {},
|
||||
}
|
||||
@@ -1,47 +1,96 @@
|
||||
import type { Config } from 'payload'
|
||||
import type { AfterErrorHook, AfterErrorHookArgs, Config, PayloadRequest } from 'payload'
|
||||
|
||||
import { defaults } from 'payload'
|
||||
import { APIError, defaults } from 'payload'
|
||||
|
||||
import { sentryPlugin } from './plugin'
|
||||
import { sentryPlugin } from './index'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
describe('plugin', () => {
|
||||
const mockExceptionID = randomUUID()
|
||||
|
||||
const mockSentry = {
|
||||
captureException() {
|
||||
return mockExceptionID
|
||||
},
|
||||
}
|
||||
|
||||
describe('@payloadcms/plugin-sentry - unit', () => {
|
||||
it('should run the plugin', () => {
|
||||
const plugin = sentryPlugin({ dsn: 'asdf', enabled: true })
|
||||
const plugin = sentryPlugin({ Sentry: mockSentry, enabled: true })
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertPluginRan(config)
|
||||
})
|
||||
|
||||
it('should default enable: true', () => {
|
||||
const plugin = sentryPlugin({ dsn: 'asdf' })
|
||||
it('should default enabled: true', () => {
|
||||
const plugin = sentryPlugin({ Sentry: mockSentry })
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertPluginRan(config)
|
||||
})
|
||||
|
||||
it('should not run if dsn is not provided', () => {
|
||||
const plugin = sentryPlugin({ dsn: null, enabled: true })
|
||||
it('should not run if Sentry is not provided', () => {
|
||||
const plugin = sentryPlugin({ enabled: true })
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertPluginDidNotRun(config)
|
||||
})
|
||||
|
||||
it('should respect enabled: false', () => {
|
||||
const plugin = sentryPlugin({ dsn: null, enabled: false })
|
||||
const plugin = sentryPlugin({ Sentry: mockSentry, enabled: false })
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertPluginDidNotRun(config)
|
||||
})
|
||||
|
||||
it('should execute Sentry.captureException with correct errors / args', async () => {
|
||||
const hintTimestamp = Date.now()
|
||||
|
||||
const plugin = sentryPlugin({
|
||||
Sentry: mockSentry,
|
||||
options: {
|
||||
context: ({ defaultContext }) => ({
|
||||
...defaultContext,
|
||||
extra: {
|
||||
...defaultContext.extra,
|
||||
hintTimestamp,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
const config = plugin(createConfig())
|
||||
|
||||
const hook = config.hooks?.afterError?.[0] as AfterErrorHook
|
||||
|
||||
const error = new APIError('ApiError', 500)
|
||||
|
||||
const afterErrorHookArgs: AfterErrorHookArgs = {
|
||||
req: {} as PayloadRequest,
|
||||
context: {},
|
||||
error,
|
||||
collection: { slug: 'mock-slug' } as any,
|
||||
}
|
||||
|
||||
const captureExceptionSpy = jest.spyOn(mockSentry, 'captureException')
|
||||
|
||||
await hook(afterErrorHookArgs)
|
||||
|
||||
expect(captureExceptionSpy).toHaveBeenCalledTimes(1)
|
||||
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
|
||||
extra: {
|
||||
errorCollectionSlug: 'mock-slug',
|
||||
hintTimestamp,
|
||||
},
|
||||
})
|
||||
expect(captureExceptionSpy).toHaveReturnedWith(mockExceptionID)
|
||||
})
|
||||
})
|
||||
|
||||
function assertPluginRan(config: Config) {
|
||||
expect(config.hooks?.afterError).toBeDefined()
|
||||
expect(config.onInit).toBeDefined()
|
||||
expect(config.hooks?.afterError?.[0]).toBeDefined()
|
||||
}
|
||||
|
||||
function assertPluginDidNotRun(config: Config) {
|
||||
expect(config.hooks?.afterError).toBeUndefined()
|
||||
expect(config.onInit).toBeUndefined()
|
||||
expect(config.hooks?.afterError?.[0]).toBeUndefined()
|
||||
}
|
||||
|
||||
function createConfig(overrides?: Partial<Config>): Config {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
import type { PluginOptions } from './types.js'
|
||||
|
||||
import { captureException } from './captureException.js'
|
||||
import { startSentry } from './startSentry.js'
|
||||
|
||||
export const sentryPlugin =
|
||||
(pluginOptions: PluginOptions) =>
|
||||
(incomingConfig: Config): Config => {
|
||||
const config = { ...incomingConfig }
|
||||
|
||||
if (pluginOptions.enabled === false || !pluginOptions.dsn) {
|
||||
return config
|
||||
}
|
||||
|
||||
config.hooks = {
|
||||
...(incomingConfig.hooks || {}),
|
||||
|
||||
afterError: [
|
||||
({ error }) => {
|
||||
captureException(error)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
config.onInit = async (payload) => {
|
||||
if (incomingConfig.onInit) {
|
||||
await incomingConfig.onInit(payload)
|
||||
}
|
||||
startSentry(pluginOptions, payload)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
12
packages/plugin-sentry/src/providers/AdminErrorBoundary.tsx
Normal file
12
packages/plugin-sentry/src/providers/AdminErrorBoundary.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { ErrorBoundary } from '@sentry/nextjs'
|
||||
|
||||
/**
|
||||
* Captures errored components to Sentry
|
||||
*/
|
||||
export const AdminErrorBoundary = ({ children }: { children: ReactNode }) => {
|
||||
return <ErrorBoundary>{children}</ErrorBoundary>
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { NextFunction, Request, Response } from 'express'
|
||||
import type express from 'express'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import * as Sentry from '@sentry/node'
|
||||
|
||||
import type { PluginOptions } from './types'
|
||||
|
||||
export const startSentry = (pluginOptions: PluginOptions, payload: Payload): void => {
|
||||
const { dsn, options } = pluginOptions
|
||||
const { express: app } = payload
|
||||
|
||||
if (!dsn || !app) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Sentry.init({
|
||||
...options?.init,
|
||||
dsn,
|
||||
integrations: [
|
||||
...(options?.init?.integrations || []),
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Sentry.Integrations.Express({ app }),
|
||||
...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(),
|
||||
],
|
||||
})
|
||||
|
||||
app.use(Sentry.Handlers.requestHandler(options?.requestHandler || {}) as express.RequestHandler)
|
||||
app.use(Sentry.Handlers.tracingHandler())
|
||||
|
||||
app.use(
|
||||
Sentry.Handlers.errorHandler({
|
||||
shouldHandleError(error) {
|
||||
if (error.status === 500) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
options?.captureErrors &&
|
||||
typeof error.status === 'number' &&
|
||||
options.captureErrors.includes(error.status)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}) as express.ErrorRequestHandler,
|
||||
)
|
||||
|
||||
app.use(function onError(
|
||||
_err: unknown,
|
||||
_req: Request,
|
||||
res: { sentry?: string } & Response,
|
||||
_next: NextFunction,
|
||||
) {
|
||||
res.statusCode = 500
|
||||
res.end(res.sentry + '\n')
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
console.log('There was an error initializing Sentry, please ensure you entered a valid DSN')
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,46 @@
|
||||
import type { RequestHandlerOptions } from '@sentry/node/types/handlers'
|
||||
import type { ClientOptions } from '@sentry/types'
|
||||
import type { ScopeContext } from '@sentry/types'
|
||||
import type { AfterErrorHookArgs } from 'payload'
|
||||
|
||||
type SentryInstance = {
|
||||
captureException: (err: Error, hint: any) => string
|
||||
}
|
||||
|
||||
type ContextArgs = {
|
||||
defaultContext: Partial<ScopeContext>
|
||||
} & AfterErrorHookArgs
|
||||
|
||||
export interface PluginOptions {
|
||||
/**
|
||||
* Sentry DSN (Data Source Name)
|
||||
* This is required unless enabled is set to false.
|
||||
* Sentry automatically assigns a DSN when you create a project.
|
||||
* If you don't have a DSN yet, you can create a new project here: https://sentry.io
|
||||
*/
|
||||
dsn: null | string
|
||||
/**
|
||||
* Enable or disable Sentry plugin
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean
|
||||
/**
|
||||
* Options passed directly to Sentry
|
||||
* @default false
|
||||
*/
|
||||
options?: {
|
||||
/**
|
||||
* Sentry will only capture 500 errors by default.
|
||||
* If you want to capture other errors, you can add them as an array here.
|
||||
* @default []
|
||||
*/
|
||||
captureErrors?: number[]
|
||||
/**
|
||||
* Passes any valid options to Sentry.init()
|
||||
* Set `ScopeContext` for `Sentry.captureException` which includes `user` and other info.
|
||||
*/
|
||||
init?: Partial<ClientOptions>
|
||||
context?: (args: ContextArgs) => Partial<ScopeContext> | Promise<Partial<ScopeContext>>
|
||||
/**
|
||||
* Passes any valid options to Sentry.Handlers.requestHandler()
|
||||
* Log captured exceptions,
|
||||
* @default false
|
||||
*/
|
||||
requestHandler?: RequestHandlerOptions
|
||||
debug?: boolean
|
||||
}
|
||||
/**
|
||||
* Instance of Sentry from
|
||||
* ```ts
|
||||
* import * as Sentry from '@sentry/nextjs'
|
||||
* ```
|
||||
* This is required unless enabled is set to false.
|
||||
*/
|
||||
Sentry?: SentryInstance
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"strict": true
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/next": "workspace:*",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/lodash.get": "^4.4.7",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
|
||||
1707
pnpm-lock.yaml
generated
1707
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
26
sentry.client.config.ts
Normal file
26
sentry.client.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
// Replay may only be enabled for the client-side
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for tracing.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
enabled: !!dsn,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// ...
|
||||
|
||||
// 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
|
||||
})
|
||||
18
sentry.server.config.ts
Normal file
18
sentry.server.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
|
||||
const enabled = !!dsn
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled,
|
||||
skipOpenTelemetrySetup: true,
|
||||
tracesSampleRate: 1.0,
|
||||
})
|
||||
|
||||
if (enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Sentry inited')
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -56,6 +56,7 @@
|
||||
"@payloadcms/storage-vercel-blob": "workspace:*",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"@sentry/nextjs": "^8.33.1",
|
||||
"@sentry/react": "^7.77.0",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import React from 'react'
|
||||
export const testErrors = () => {
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export const TestErrors = () => {
|
||||
const [throwClientSide, setThrowClientSide] = useState(false)
|
||||
|
||||
const notFound = async () => {
|
||||
const req = await fetch('http://localhost:3000/api/users/notFound', {
|
||||
method: 'GET',
|
||||
@@ -60,8 +63,12 @@ export const testErrors = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const ThrowClientSide = () => {
|
||||
throw new Error('client side error')
|
||||
}
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary>
|
||||
<>
|
||||
<h4>Test Errors</h4>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button onClick={() => notFound()} style={{ marginBottom: '20px' }} type="button">
|
||||
@@ -87,7 +94,15 @@ export const testErrors = () => {
|
||||
<button onClick={() => badVerify()} style={{ marginBottom: '20px' }} type="button">
|
||||
Bad Verify
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setThrowClientSide(true)}
|
||||
style={{ marginBottom: '20px' }}
|
||||
type="button"
|
||||
>
|
||||
Throw client side error
|
||||
</button>
|
||||
{throwClientSide && <ThrowClientSide />}
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import { sentryPlugin } from '@payloadcms/plugin-sentry'
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { APIError } from 'payload'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
@@ -12,7 +14,7 @@ import { Users } from './collections/Users.js'
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
components: {
|
||||
beforeDashboard: ['/components.js#testErrors'],
|
||||
beforeDashboard: ['/TestErrors.js#TestErrors'],
|
||||
},
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
@@ -29,17 +31,21 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
})
|
||||
},
|
||||
endpoints: [
|
||||
{
|
||||
path: '/exception',
|
||||
handler: () => {
|
||||
throw new APIError('Test Plugin-Sentry Exception', 500)
|
||||
},
|
||||
method: 'get',
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
sentryPlugin({
|
||||
dsn: 'https://61edebe5ee6d4d38a9d6459c7323d777@o4505289711681536.ingest.sentry.io/4505357688242176',
|
||||
Sentry,
|
||||
options: {
|
||||
captureErrors: [400, 403, 404],
|
||||
init: {
|
||||
debug: true,
|
||||
},
|
||||
requestHandler: {
|
||||
serverName: false,
|
||||
},
|
||||
captureErrors: [400, 403, 404],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface Config {
|
||||
collections: {
|
||||
posts: Post;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
@@ -70,6 +71,29 @@ export interface User {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
|
||||
@@ -78,6 +78,9 @@
|
||||
"@payloadcms/plugin-seo/client": [
|
||||
"./packages/plugin-seo/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-sentry/client": [
|
||||
"./packages/plugin-sentry/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-stripe/client": [
|
||||
"./packages/plugin-stripe/src/exports/client.ts"
|
||||
],
|
||||
@@ -171,6 +174,9 @@
|
||||
"app",
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"scripts/**/*.ts"
|
||||
"scripts/**/*.ts",
|
||||
"instrumentation.ts",
|
||||
"sentry.server.config.ts",
|
||||
"sentry.client.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user