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:
Sasha
2024-10-09 21:26:58 +03:00
committed by GitHub
parent 769c94b4fd
commit 0b2a7a3606
28 changed files with 1867 additions and 450 deletions

27
app/global-error.tsx Normal file
View 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>
)
}

View File

@@ -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 - **Integrations**: Connects with various tools and services for enhanced workflow and issue management
<Banner type="info"> <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> </Banner>
## Installation ## Installation
@@ -42,6 +42,15 @@ Install the plugin using any JavaScript package manager like [Yarn](https://yarn
pnpm add @payloadcms/plugin-sentry 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 ## 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. 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 { sentryPlugin } from '@payloadcms/plugin-sentry'
import { Pages, Media } from './collections' import { Pages, Media } from './collections'
import * as Sentry from '@sentry/nextjs'
const config = buildConfig({ const config = buildConfig({
collections: [Pages, Media], collections: [Pages, Media],
plugins: [ plugins: [
sentryPlugin({ sentryPlugin({
dsn: 'https://61edebas776889984d323d777@o4505289711681536.ingest.sentry.io/4505357433352176', Sentry,
}), }),
], ],
}) })
@@ -65,58 +76,55 @@ export default config
## Options ## 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"> <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> </Banner>
- `enabled`: boolean | optional - `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). Pass additional [contextual data](https://docs.sentry.io/platforms/javascript/enriching-events/context/#passing-context-directly) to Sentry
- `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).
- `captureErrors`: number[] | optional - `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. 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 ### Example
Configure any of these options by passing them to the plugin: Configure any of these options by passing them to the plugin:
```ts ```ts
import { buildConfig } from 'payload' 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' import { Pages, Media } from './collections'
const config = buildConfig({ const config = buildConfig({
collections: [Pages, Media], collections: [Pages, Media],
plugins: [ plugins: [
sentry({ sentryPlugin({
dsn: 'https://61edebas777689984d323d777@o4505289711681536.ingest.sentry.io/4505357433352176',
options: { options: {
init: { captureErrors: [400, 403],
debug: true, context: ({ defaultContext, req }) => {
environment: 'development', return {
tracesSampleRate: 1.0, ...defaultContext,
tags: {
locale: req.locale,
},
}
}, },
requestHandler: { debug: true,
serverName: false,
user: ['email'],
},
captureErrors: [400, 403, 404],
}, },
Sentry,
}), }),
], ],
}) })

5
instrumentation.ts Normal file
View File

@@ -0,0 +1,5 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config.js')
}
}

View File

@@ -1,6 +1,6 @@
import bundleAnalyzer from '@next/bundle-analyzer' import bundleAnalyzer from '@next/bundle-analyzer'
import { withSentryConfig } from '@sentry/nextjs'
import withPayload from './packages/next/src/withPayload.js' import { withPayload } from './packages/next/src/withPayload.js'
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -11,8 +11,7 @@ const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true', enabled: process.env.ANALYZE === 'true',
}) })
// eslint-disable-next-line no-restricted-exports const config = withBundleAnalyzer(
export default withBundleAnalyzer(
withPayload({ withPayload({
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
@@ -23,7 +22,6 @@ export default withBundleAnalyzer(
env: { env: {
PAYLOAD_CORE_DEV: 'true', PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname), ROOT_DIR: path.resolve(dirname),
PAYLOAD_CI_DEPENDENCY_CHECKER: 'true',
}, },
async redirects() { async redirects() {
return [ return [
@@ -48,3 +46,8 @@ export default withBundleAnalyzer(
}, },
}), }),
) )
export default withSentryConfig(config, {
telemetry: false,
tunnelRoute: '/monitoring-tunnel',
})

View File

@@ -111,6 +111,8 @@
"@payloadcms/eslint-plugin": "workspace:*", "@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*", "@payloadcms/live-preview-react": "workspace:*",
"@playwright/test": "1.46.0", "@playwright/test": "1.46.0",
"@sentry/nextjs": "^8.33.1",
"@sentry/node": "^8.33.1",
"@swc-node/register": "1.10.9", "@swc-node/register": "1.10.9",
"@swc/cli": "0.4.0", "@swc/cli": "0.4.0",
"@swc/jest": "0.2.36", "@swc/jest": "0.2.36",

View File

@@ -60,7 +60,6 @@
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"@types/express": "^4.17.21",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",

View File

@@ -50,9 +50,6 @@
}, },
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@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:*" "payload": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
@@ -74,9 +71,5 @@
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
}, },
"homepage:": "https://payloadcms.com", "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"
}
} }

View File

@@ -55,7 +55,6 @@
}, },
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@types/express": "^4.17.9",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"payload": "workspace:*" "payload": "workspace:*"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/plugin-sentry", "name": "@payloadcms/plugin-sentry",
"version": "0.0.6", "version": "3.0.0-beta.111",
"description": "Sentry plugin for Payload", "description": "Sentry plugin for Payload",
"keywords": [ "keywords": [
"payload", "payload",
@@ -23,6 +23,11 @@
"import": "./src/index.ts", "import": "./src/index.ts",
"types": "./src/index.ts", "types": "./src/index.ts",
"default": "./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", "main": "./src/index.ts",
@@ -31,32 +36,24 @@
"dist" "dist"
], ],
"scripts": { "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:swc": "swc ./src -d ./dist --config-file .swcrc-build --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist", "build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}", "clean": "rimraf {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build" "prepublishOnly": "pnpm clean && pnpm turbo build"
}, },
"dependencies": { "dependencies": {
"@sentry/node": "^7.55.2", "@sentry/nextjs": "^8.33.1",
"@sentry/types": "^7.54.0", "@sentry/types": "^8.33.1"
"express": "^4.18.2"
}, },
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@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": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"copyfiles": "^2.4.1", "payload": "workspace:*"
"cross-env": "^7.0.3",
"jest": "^29.7.0",
"nodemon": "3.0.3",
"payload": "workspace:*",
"ts-jest": "^29.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"payload": "workspace:*", "payload": "workspace:*",
@@ -69,6 +66,11 @@
"import": "./dist/index.js", "import": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"default": "./dist/index.js" "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", "main": "./dist/index.js",

View File

@@ -1,5 +0,0 @@
import * as Sentry from '@sentry/node'
export const captureException = (err: Error): void => {
Sentry.captureException(err)
}

View File

@@ -0,0 +1 @@
export { AdminErrorBoundary } from '../providers/AdminErrorBoundary.js'

View File

@@ -1,2 +1,94 @@
export { sentryPlugin } from './plugin.js' import type { ScopeContext } from '@sentry/types'
export type { PluginOptions } from './types.js' 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}`,
)
}
}
}
},
],
},
}
}

View File

@@ -1,4 +0,0 @@
module.exports = {
captureException: () => {},
startSentry: () => {},
}

View File

@@ -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', () => { it('should run the plugin', () => {
const plugin = sentryPlugin({ dsn: 'asdf', enabled: true }) const plugin = sentryPlugin({ Sentry: mockSentry, enabled: true })
const config = plugin(createConfig()) const config = plugin(createConfig())
assertPluginRan(config) assertPluginRan(config)
}) })
it('should default enable: true', () => { it('should default enabled: true', () => {
const plugin = sentryPlugin({ dsn: 'asdf' }) const plugin = sentryPlugin({ Sentry: mockSentry })
const config = plugin(createConfig()) const config = plugin(createConfig())
assertPluginRan(config) assertPluginRan(config)
}) })
it('should not run if dsn is not provided', () => { it('should not run if Sentry is not provided', () => {
const plugin = sentryPlugin({ dsn: null, enabled: true }) const plugin = sentryPlugin({ enabled: true })
const config = plugin(createConfig()) const config = plugin(createConfig())
assertPluginDidNotRun(config) assertPluginDidNotRun(config)
}) })
it('should respect enabled: false', () => { it('should respect enabled: false', () => {
const plugin = sentryPlugin({ dsn: null, enabled: false }) const plugin = sentryPlugin({ Sentry: mockSentry, enabled: false })
const config = plugin(createConfig()) const config = plugin(createConfig())
assertPluginDidNotRun(config) 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) { function assertPluginRan(config: Config) {
expect(config.hooks?.afterError).toBeDefined() expect(config.hooks?.afterError?.[0]).toBeDefined()
expect(config.onInit).toBeDefined()
} }
function assertPluginDidNotRun(config: Config) { function assertPluginDidNotRun(config: Config) {
expect(config.hooks?.afterError).toBeUndefined() expect(config.hooks?.afterError?.[0]).toBeUndefined()
expect(config.onInit).toBeUndefined()
} }
function createConfig(overrides?: Partial<Config>): Config { function createConfig(overrides?: Partial<Config>): Config {

View File

@@ -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
}

View 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>
}

View File

@@ -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')
}
}

View File

@@ -1,36 +1,46 @@
import type { RequestHandlerOptions } from '@sentry/node/types/handlers' import type { ScopeContext } from '@sentry/types'
import type { ClientOptions } 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 { 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 * Enable or disable Sentry plugin
* @default false * @default true
*/ */
enabled?: boolean enabled?: boolean
/** /**
* Options passed directly to Sentry * Options passed directly to Sentry
* @default false
*/ */
options?: { options?: {
/** /**
* Sentry will only capture 500 errors by default. * Sentry will only capture 500 errors by default.
* If you want to capture other errors, you can add them as an array here. * If you want to capture other errors, you can add them as an array here.
* @default []
*/ */
captureErrors?: number[] 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
} }

View File

@@ -5,8 +5,8 @@
"noEmit": false /* Do not emit outputs. */, "noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */, "outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */, "rootDir": "./src", /* Specify the root folder within your source files. */
"strict": true "jsx": "react-jsx"
}, },
"exclude": [ "exclude": [
"dist", "dist",

View File

@@ -63,7 +63,6 @@
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*", "@payloadcms/next": "workspace:*",
"@types/express": "^4.17.9",
"@types/lodash.get": "^4.4.7", "@types/lodash.get": "^4.4.7",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",

1707
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

26
sentry.client.config.ts Normal file
View 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
View 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 {}

View File

@@ -56,6 +56,7 @@
"@payloadcms/storage-vercel-blob": "workspace:*", "@payloadcms/storage-vercel-blob": "workspace:*",
"@payloadcms/translations": "workspace:*", "@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*", "@payloadcms/ui": "workspace:*",
"@sentry/nextjs": "^8.33.1",
"@sentry/react": "^7.77.0", "@sentry/react": "^7.77.0",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",

View File

@@ -1,7 +1,10 @@
'use client' 'use client'
import * as Sentry from '@sentry/react'
import React from 'react' import { useState } from 'react'
export const testErrors = () => {
export const TestErrors = () => {
const [throwClientSide, setThrowClientSide] = useState(false)
const notFound = async () => { const notFound = async () => {
const req = await fetch('http://localhost:3000/api/users/notFound', { const req = await fetch('http://localhost:3000/api/users/notFound', {
method: 'GET', method: 'GET',
@@ -60,8 +63,12 @@ export const testErrors = () => {
}) })
} }
const ThrowClientSide = () => {
throw new Error('client side error')
}
return ( return (
<Sentry.ErrorBoundary> <>
<h4>Test Errors</h4> <h4>Test Errors</h4>
<div style={{ display: 'flex', gap: '10px' }}> <div style={{ display: 'flex', gap: '10px' }}>
<button onClick={() => notFound()} style={{ marginBottom: '20px' }} type="button"> <button onClick={() => notFound()} style={{ marginBottom: '20px' }} type="button">
@@ -87,7 +94,15 @@ export const testErrors = () => {
<button onClick={() => badVerify()} style={{ marginBottom: '20px' }} type="button"> <button onClick={() => badVerify()} style={{ marginBottom: '20px' }} type="button">
Bad Verify Bad Verify
</button> </button>
<button
onClick={() => setThrowClientSide(true)}
style={{ marginBottom: '20px' }}
type="button"
>
Throw client side error
</button>
{throwClientSide && <ThrowClientSide />}
</div> </div>
</Sentry.ErrorBoundary> </>
) )
} }

View File

@@ -3,6 +3,8 @@ import path from 'path'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
import { sentryPlugin } from '@payloadcms/plugin-sentry' import { sentryPlugin } from '@payloadcms/plugin-sentry'
import * as Sentry from '@sentry/nextjs'
import { APIError } from 'payload'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js' import { devUser } from '../credentials.js'
@@ -12,7 +14,7 @@ import { Users } from './collections/Users.js'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
admin: { admin: {
components: { components: {
beforeDashboard: ['/components.js#testErrors'], beforeDashboard: ['/TestErrors.js#TestErrors'],
}, },
importMap: { importMap: {
baseDir: path.resolve(dirname), 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: [ plugins: [
sentryPlugin({ sentryPlugin({
dsn: 'https://61edebe5ee6d4d38a9d6459c7323d777@o4505289711681536.ingest.sentry.io/4505357688242176', Sentry,
options: { options: {
debug: true,
captureErrors: [400, 403, 404], captureErrors: [400, 403, 404],
init: {
debug: true,
},
requestHandler: {
serverName: false,
},
}, },
}), }),
], ],

View File

@@ -13,6 +13,7 @@ export interface Config {
collections: { collections: {
posts: Post; posts: Post;
users: User; users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
@@ -70,6 +71,29 @@ export interface User {
lockUntil?: string | null; lockUntil?: string | null;
password?: 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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".

View File

@@ -78,6 +78,9 @@
"@payloadcms/plugin-seo/client": [ "@payloadcms/plugin-seo/client": [
"./packages/plugin-seo/src/exports/client.ts" "./packages/plugin-seo/src/exports/client.ts"
], ],
"@payloadcms/plugin-sentry/client": [
"./packages/plugin-sentry/src/exports/client.ts"
],
"@payloadcms/plugin-stripe/client": [ "@payloadcms/plugin-stripe/client": [
"./packages/plugin-stripe/src/exports/client.ts" "./packages/plugin-stripe/src/exports/client.ts"
], ],
@@ -171,6 +174,9 @@
"app", "app",
"next-env.d.ts", "next-env.d.ts",
".next/types/**/*.ts", ".next/types/**/*.ts",
"scripts/**/*.ts" "scripts/**/*.ts",
"instrumentation.ts",
"sentry.server.config.ts",
"sentry.client.config.ts"
] ]
} }