diff --git a/dev/test-studio/plugins/error-reporting-test/ErrorReportingTest.tsx b/dev/test-studio/plugins/error-reporting-test/ErrorReportingTest.tsx
new file mode 100644
index 00000000000..f3333fb0fcc
--- /dev/null
+++ b/dev/test-studio/plugins/error-reporting-test/ErrorReportingTest.tsx
@@ -0,0 +1,73 @@
+import {Button, Card, Flex, Stack} from '@sanity/ui'
+import {useCallback, useState} from 'react'
+
+function triggerCustomErrorOnEvent() {
+ throw new Error('Custom error triggered')
+}
+
+function triggerTypeErrorOnEvent(evt: any) {
+ evt.someFunctionThatDoesntExist()
+}
+
+function triggerTimeoutError() {
+ setTimeout(() => {
+ throw new Error('Custom error in setTimeout')
+ }, 1000)
+}
+
+function triggerPromiseError() {
+ return new Promise((resolve, reject) => {
+ requestAnimationFrame(() => {
+ reject(new Error('Custom error in promise'))
+ })
+ })
+}
+
+export function ErrorReportingTest() {
+ const [doRenderError, setRenderError] = useState(false)
+ const handleShouldRenderWithError = useCallback(() => setRenderError(true), [])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {doRenderError && }
+
+ )
+}
+
+function WithRenderError({text}: any) {
+ return
{text.toUpperCase()}
+}
diff --git a/dev/test-studio/plugins/error-reporting-test/index.ts b/dev/test-studio/plugins/error-reporting-test/index.ts
new file mode 100644
index 00000000000..a0ed13843f9
--- /dev/null
+++ b/dev/test-studio/plugins/error-reporting-test/index.ts
@@ -0,0 +1 @@
+export * from './plugin'
diff --git a/dev/test-studio/plugins/error-reporting-test/plugin.tsx b/dev/test-studio/plugins/error-reporting-test/plugin.tsx
new file mode 100644
index 00000000000..49d04968400
--- /dev/null
+++ b/dev/test-studio/plugins/error-reporting-test/plugin.tsx
@@ -0,0 +1,20 @@
+import {AsteriskIcon} from '@sanity/icons'
+import {definePlugin} from 'sanity'
+import {route} from 'sanity/router'
+
+import {ErrorReportingTest} from './ErrorReportingTest'
+
+export const errorReportingTestPlugin = definePlugin(() => {
+ return {
+ name: 'error-reporting-test',
+ tools: [
+ {
+ name: 'error-reporting-test',
+ title: 'Errors test',
+ icon: AsteriskIcon,
+ component: ErrorReportingTest,
+ router: route.create('/'),
+ },
+ ],
+ }
+})
diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts
index 57f81691b5f..536f76a1a45 100644
--- a/dev/test-studio/sanity.config.ts
+++ b/dev/test-studio/sanity.config.ts
@@ -43,6 +43,7 @@ import {pasteAction} from './fieldActions/pasteAction'
import {resolveInitialValueTemplates} from './initialValueTemplates'
import {customInspector} from './inspectors/custom'
import {testStudioLocaleBundles} from './locales'
+import {errorReportingTestPlugin} from './plugins/error-reporting-test'
import {languageFilter} from './plugins/language-filter'
import {presenceTool} from './plugins/presence'
import {routerDebugTool} from './plugins/router-debug'
@@ -135,6 +136,7 @@ const sharedSettings = definePlugin({
imageHotspotArrayPlugin(),
presenceTool(),
routerDebugTool(),
+ errorReportingTestPlugin(),
tsdoc(),
],
})
diff --git a/packages/sanity/package.json b/packages/sanity/package.json
index c974727c3ac..3308f505347 100644
--- a/packages/sanity/package.json
+++ b/packages/sanity/package.json
@@ -170,6 +170,7 @@
"@sanity/ui": "^2.4.0",
"@sanity/util": "3.46.1",
"@sanity/uuid": "^3.0.1",
+ "@sentry/react": "^8.7.0",
"@tanstack/react-table": "^8.16.0",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@types/react-copy-to-clipboard": "^5.0.2",
diff --git a/packages/sanity/src/core/error/errorReporter.ts b/packages/sanity/src/core/error/errorReporter.ts
new file mode 100644
index 00000000000..1ba3fb99c2f
--- /dev/null
+++ b/packages/sanity/src/core/error/errorReporter.ts
@@ -0,0 +1,36 @@
+import {type ErrorInfo as ReactErrorInfo} from 'react'
+
+import {getSentryErrorReporter} from './sentry/sentryErrorReporter'
+
+/**
+ * @internal
+ */
+export interface ErrorInfo {
+ reactErrorInfo?: ReactErrorInfo
+ errorBoundary?: string
+}
+
+/**
+ * @internal
+ */
+export interface ErrorReporter {
+ /** Call to prepare the error reporter for use */
+ initialize: () => void
+
+ /**
+ * Reports an error, as caught by an error handler or a React boundary.
+ *
+ * @param error - The error that is caught. Note that while typed as `Error` by Reacts `componentDidCatch`, it can also be invoked with non-error objects.
+ * @param options - Additional options for the error report
+ * @returns An object containing information on the reported error, or `null` if ignored
+ */
+ reportError: (error: Error, options?: ErrorInfo) => {eventId: string} | null
+}
+
+/**
+ * Singleton instance of an error reporter, that will send errors encountered during execution or
+ * rendering to Sanity (potentially to a third party error tracking service).
+ *
+ * @internal
+ */
+export const errorReporter = getSentryErrorReporter()
diff --git a/packages/sanity/src/core/error/sentry/README.md b/packages/sanity/src/core/error/sentry/README.md
new file mode 100644
index 00000000000..2e066f39f16
--- /dev/null
+++ b/packages/sanity/src/core/error/sentry/README.md
@@ -0,0 +1,3 @@
+This may be moved into a separate package in the future, in order for us to provide a more generic error reporter that may be swapped for a different service if need be.
+
+For now, we keep it in-module to keep things simple.
diff --git a/packages/sanity/src/core/error/sentry/sentryErrorReporter.ts b/packages/sanity/src/core/error/sentry/sentryErrorReporter.ts
new file mode 100644
index 00000000000..42550bd78f4
--- /dev/null
+++ b/packages/sanity/src/core/error/sentry/sentryErrorReporter.ts
@@ -0,0 +1,375 @@
+import {
+ breadcrumbsIntegration,
+ browserApiErrorsIntegration,
+ BrowserClient,
+ type BrowserOptions,
+ captureException,
+ dedupeIntegration,
+ defaultStackParser,
+ type ErrorEvent,
+ type Event,
+ functionToStringIntegration,
+ getClient,
+ getCurrentScope,
+ globalHandlersIntegration,
+ httpContextIntegration,
+ inboundFiltersIntegration,
+ init,
+ isInitialized as sentryIsInitialized,
+ linkedErrorsIntegration,
+ makeFetchTransport,
+ Scope,
+ withScope,
+} from '@sentry/react'
+
+import {isDev} from '../../environment'
+import {hasSanityPackageInImportMap} from '../../environment/hasSanityPackageInImportMap'
+import {globalScope} from '../../util/globalScope'
+import {supportsLocalStorage} from '../../util/supportsLocalStorage'
+import {SANITY_VERSION} from '../../version'
+import {type ErrorInfo, type ErrorReporter} from '../errorReporter'
+
+const SANITY_DSN = 'https://8914c8dde7e1ebce191f15af8bf6b7b9@sentry.sanity.io/4507342122123264'
+
+const IS_EMBEDDED_STUDIO = !('__sanityErrorChannel' in globalScope)
+
+const DEBUG_ERROR_REPORTING =
+ supportsLocalStorage && Boolean(localStorage.getItem('SANITY_DEBUG_ERROR_REPORTING'))
+
+const IS_BROWSER = typeof window !== 'undefined'
+
+const clientOptions: BrowserOptions = {
+ dsn: SANITY_DSN,
+ release: SANITY_VERSION,
+ environment: isDev ? 'development' : 'production',
+ debug: DEBUG_ERROR_REPORTING,
+ enabled: IS_BROWSER && (!isDev || DEBUG_ERROR_REPORTING),
+}
+
+const integrations = [
+ inboundFiltersIntegration(),
+ functionToStringIntegration(),
+ browserApiErrorsIntegration({eventTarget: false}),
+ breadcrumbsIntegration({console: false}),
+ globalHandlersIntegration({onerror: true, onunhandledrejection: true}),
+ linkedErrorsIntegration(),
+ dedupeIntegration(),
+ sanityDedupeIntegration(),
+ httpContextIntegration(),
+]
+
+/**
+ * Get an instance of the Sentry error reporter
+ *
+ * @internal
+ */
+export function getSentryErrorReporter(): ErrorReporter {
+ let client: BrowserClient | undefined
+ let scope: Scope | undefined
+
+ function initialize() {
+ // If this _Sanity_ implementation of the reporter is already initialized, do not re-instantiate
+ if (client) {
+ return
+ }
+
+ // For now, we only want to run error reporting for auto-updating studios in production.
+ // This may change in the future, but for now this will help us control the amount of errors.
+ if (!DEBUG_ERROR_REPORTING && !hasSanityPackageInImportMap()) {
+ return
+ }
+
+ // For now, we also want to avoid running error reporting in embedded studios,
+ // even if it has a Sanity package in the import map (eg. is auto updating).
+ if (!DEBUG_ERROR_REPORTING && IS_EMBEDDED_STUDIO) {
+ return
+ }
+
+ // This normally shouldn't happen, but if we're initialized and already using the Sanity DSN,
+ // then assume we can reuse the global client
+ const isSentryInitialized = sentryIsInitialized()
+ const hasThirdPartySentry = isSentryInitialized && getClient()?.getOptions().dsn === SANITY_DSN
+ if (isSentryInitialized && !hasThirdPartySentry) {
+ client = getClient()
+ scope = getCurrentScope()
+ return
+ }
+
+ // "Third party" means the customer already has an instance of the Sentry SDK on the page,
+ // but it is not configured to use the Sanity DSN. In this case, we'll create a new client
+ // for ourselves, and try to avoid the global scope.
+ if (hasThirdPartySentry) {
+ client = new BrowserClient({
+ ...clientOptions,
+ transport: makeFetchTransport,
+ stackParser: defaultStackParser,
+ integrations,
+ beforeSend,
+ })
+
+ scope = new Scope()
+ scope.setClient(client)
+
+ // Initializing has to be done after setting the client on the scope
+ client.init()
+ return
+ }
+
+ // There is no active client on the page, so assume we can take ownership of the
+ // global scope and client. This is the default, recommended behavior for the Sentry client,
+ // and as such is what we primarily want to rely on.
+ init({
+ ...clientOptions,
+ defaultIntegrations: false,
+ integrations,
+ beforeSend,
+ })
+
+ client = getClient()
+ scope = getCurrentScope()
+ }
+
+ function reportError(error: Error, options: ErrorInfo = {}) {
+ if (!client) {
+ console.warn('[reportError] called before reporter is initialized, skipping')
+ return null
+ }
+
+ const {reactErrorInfo = {}, errorBoundary} = options
+ const {componentStack} = reactErrorInfo
+
+ // Decorate the error report with relevant context and tags
+ const contexts: Record | undefined> = {}
+ if (componentStack) {
+ contexts.react = {componentStack}
+ }
+
+ const tags: {[key: string]: number | string | boolean | null | undefined} = {
+ handled: 'no',
+ }
+
+ if (errorBoundary) {
+ tags.errorBoundary = errorBoundary
+ }
+
+ let eventId: string | null = null
+ withScope(() => {
+ if (componentStack && isError(error)) {
+ const errorBoundaryError = new Error(error.message)
+ errorBoundaryError.name = `${errorBoundary || 'ErrorBoundary'} ${error.name}`
+ errorBoundaryError.stack = componentStack
+
+ // Using the `LinkedErrors` integration to link the errors together.
+ setCause(error, errorBoundaryError)
+ }
+
+ eventId = captureException(error, {
+ mechanism: {handled: false},
+ captureContext: {contexts, tags},
+ })
+ })
+
+ return eventId ? {eventId} : null
+ }
+
+ return {
+ initialize,
+ reportError,
+ }
+}
+
+const objectToString = Object.prototype.toString
+
+/**
+ * Checks whether given value's type is one of a few Error or Error-like
+ *
+ * @param thing - A value to be checked
+ * @returns A boolean representing the result
+ * @internal
+ */
+function isError(thing: unknown): thing is Error & {cause?: Error} {
+ switch (objectToString.call(thing)) {
+ case '[object Error]':
+ case '[object Exception]':
+ case '[object DOMException]':
+ return true
+ default:
+ return isInstanceOf(thing, Error)
+ }
+}
+
+/**
+ * Checks whether given value's type is an instance of provided constructor.
+ *
+ * @param thing - A value to be checked.
+ * @param base - A constructor to be used in a check.
+ * @returns A boolean representing the result.
+ * @internal
+ */
+function isInstanceOf(thing: unknown, base: any): boolean {
+ try {
+ return thing instanceof base
+ } catch (_e) {
+ return false
+ }
+}
+
+/**
+ * Set the `cause` property on an error object
+ *
+ * @param error - The error to set the cause on
+ * @param cause - The cause of the error
+ * @internal
+ */
+function setCause(error: Error & {cause?: Error}, cause: Error): void {
+ const seenErrors = new WeakMap()
+
+ function recurse(err: Error & {cause?: Error | unknown}, subCause: Error): void {
+ // If we've already seen the error, there is a recursive loop somewhere in the error's
+ // cause chain. Let's just bail out then to prevent a stack overflow.
+ if (seenErrors.has(err)) {
+ return
+ }
+
+ if (isError(err.cause)) {
+ seenErrors.set(err, true)
+ recurse(err.cause, subCause)
+ return
+ }
+ err.cause = subCause
+ }
+
+ recurse(error, cause)
+}
+
+/**
+ * Sentry treats errors that are caught in an error boundary as "handled", which we don't want.
+ * It gives a false sense of security, as the error is only caught to show a more helpful error
+ * than a blank page. This function sets the `handled` prop on the error's mechanism to `false`.
+ * Note: This _mutates_ the event, in order to avoid having to deep-clone.
+ *
+ * @param event - The event to mark as unhandled
+ * @internal
+ */
+function setAsUnhandled(event: ErrorEvent) {
+ for (const exception of event.exception?.values || []) {
+ if (exception.mechanism) {
+ exception.mechanism.handled = false
+ }
+ }
+}
+
+/**
+ * "Before send" event handler, which sets the error as unhandled.
+ * @see setAsUnhandled for a clearer rationale.
+ *
+ * @param event - The event to be sent
+ * @returns The event to be sent
+ * @internal
+ */
+function beforeSend(event: ErrorEvent): ErrorEvent {
+ setAsUnhandled(event)
+ return event
+}
+
+/**
+ * We'll want a more aggressive dedupe strategy than the default one, as the default is very
+ * fine grained, needing the same exact stack and message to be considered a duplicate.
+ * We want to be more conservative.
+ *
+ * @internal
+ */
+function sanityDedupeIntegration() {
+ const previousEvents: Event[] = []
+
+ return {
+ name: 'SanityDedupe',
+ processEvent(currentEvent: Event): Event | null | PromiseLike {
+ // We want to ignore any non-error type events, e.g. transactions or replays
+ // These should never be deduped, and also not be compared against _previousEvent.
+ if (currentEvent.type) {
+ return currentEvent
+ }
+
+ // Juuust in case something goes wrong
+ try {
+ if (shouldDropEvent(currentEvent, previousEvents)) {
+ if (DEBUG_ERROR_REPORTING) {
+ console.warn(
+ '[sanity/sentry] Dropping error from being reported because it is a duplicate',
+ )
+ }
+ return null
+ }
+ } catch (_) {
+ /* empty */
+ }
+
+ // Keep the last 10 events around for comparison
+ if (previousEvents.length > 10) {
+ previousEvents.shift()
+ }
+
+ previousEvents.push(currentEvent)
+ return currentEvent
+ },
+ }
+}
+
+/**
+ * Determines wether or not the given event should be dropped or not, based on a window of
+ * previously reported events.
+ *
+ * @param currentEvent - The event to check
+ * @param previousEvents - An array of previously reported events
+ * @returns True if event should be dropped, false otherwise
+ * @internal
+ */
+function shouldDropEvent(currentEvent: Event, previousEvents: Event[]): boolean {
+ for (const previousEvent of previousEvents) {
+ const currentMessage = getMessageFromEvent(currentEvent)
+ const previousMessage = getMessageFromEvent(previousEvent)
+
+ if (currentMessage && previousMessage && currentMessage !== previousMessage) {
+ continue
+ }
+
+ // Sentry timestamps are in fractional seconds, not milliseconds
+ const currentTimestamp = Math.floor(currentEvent.timestamp || 0)
+ const previousTimestamp = Math.floor(previousEvent.timestamp || 0)
+
+ // If the events are within 5 minutes of each other, we consider them duplicates.
+ // 5 minutes is a bit much, but if an error occurs every 5 minutes, we better be
+ // investigating it - and reporting the same error from the same user every 5 minutes
+ // is not really helpful.
+ if (Math.abs(currentTimestamp - previousTimestamp) < 300) {
+ return true
+ }
+ }
+
+ return false
+}
+
+/**
+ * Extract the `message` string from a Sentry event. Sometimes this is not available on the `event`
+ * itself, but buried inside of the `event.exception` property.
+ *
+ * @param event - The Sentry event to extract the message from
+ * @returns A string representing the message, or `undefined` if not found
+ * @internal
+ */
+function getMessageFromEvent(event: Event): string | undefined {
+ if (event.message) {
+ return event.message
+ }
+
+ if (event.exception) {
+ for (const exception of event.exception.values || []) {
+ if (exception.value) {
+ return exception.value
+ }
+ }
+ }
+
+ return undefined
+}
diff --git a/packages/sanity/src/core/studio/StudioErrorBoundary.tsx b/packages/sanity/src/core/studio/StudioErrorBoundary.tsx
index 0d5bfed1b10..b82524c5422 100644
--- a/packages/sanity/src/core/studio/StudioErrorBoundary.tsx
+++ b/packages/sanity/src/core/studio/StudioErrorBoundary.tsx
@@ -1,11 +1,20 @@
/* eslint-disable i18next/no-literal-string */
/* eslint-disable @sanity/i18n/no-attribute-string-literals */
-import {Card, Code, Container, ErrorBoundary, Heading, Stack} from '@sanity/ui'
-import {type ReactNode, useCallback, useState} from 'react'
+import {
+ Card,
+ Code,
+ Container,
+ ErrorBoundary,
+ type ErrorBoundaryProps,
+ Heading,
+ Stack,
+} from '@sanity/ui'
+import {type ErrorInfo, type ReactNode, useCallback, useState} from 'react'
import {useHotModuleReload} from 'use-hot-module-reload'
import {Button} from '../../ui-components'
import {SchemaError} from '../config'
+import {errorReporter} from '../error/errorReporter'
import {CorsOriginError} from '../store'
import {isRecord} from '../util'
import {CorsOriginErrorScreen, SchemaErrorsScreen} from './screens'
@@ -15,16 +24,46 @@ interface StudioErrorBoundaryProps {
heading?: string
}
+type ErrorBoundaryState =
+ | {
+ componentStack: null
+ error: null
+ eventId: null
+ }
+ | {
+ componentStack: ErrorInfo['componentStack']
+ error: Error
+ eventId: string | null
+ }
+
+const INITIAL_STATE = {
+ componentStack: null,
+ error: null,
+ eventId: null,
+} satisfies ErrorBoundaryState
+
export function StudioErrorBoundary({
children,
heading = 'An error occured',
}: StudioErrorBoundaryProps) {
- const [{error}, setError] = useState<{error: unknown}>({error: null})
+ const [{error, eventId}, setError] = useState(INITIAL_STATE)
const message = isRecord(error) && typeof error.message === 'string' && error.message
const stack = isRecord(error) && typeof error.stack === 'string' && error.stack
- const handleResetError = useCallback(() => setError({error: null}), [])
+ const handleResetError = useCallback(() => setError(INITIAL_STATE), [])
+ const handleCatchError: ErrorBoundaryProps['onCatch'] = useCallback((params) => {
+ const report = errorReporter.reportError(params.error, {
+ reactErrorInfo: params.info,
+ errorBoundary: 'StudioErrorBoundary',
+ })
+
+ setError({
+ error: params.error,
+ componentStack: params.info.componentStack,
+ eventId: report?.eventId || null,
+ })
+ }, [])
useHotModuleReload(handleResetError)
@@ -37,7 +76,7 @@ export function StudioErrorBoundary({
}
if (!error) {
- return {children}
+ return {children}
}
return (
@@ -67,6 +106,7 @@ export function StudioErrorBoundary({
)}
{stack && {stack}
}
+ {eventId && Event ID: {eventId}
}
diff --git a/packages/sanity/src/core/studio/StudioProvider.tsx b/packages/sanity/src/core/studio/StudioProvider.tsx
index 78a33548048..b0cc2d786d7 100644
--- a/packages/sanity/src/core/studio/StudioProvider.tsx
+++ b/packages/sanity/src/core/studio/StudioProvider.tsx
@@ -9,6 +9,7 @@ import typescript from 'refractor/lang/typescript.js'
import {LoadingBlock} from '../components/loadingBlock'
import {ErrorLogger} from '../error/ErrorLogger'
+import {errorReporter} from '../error/errorReporter'
import {LocaleProvider} from '../i18n'
import {ResourceCacheProvider} from '../store'
import {UserColorManagerProvider} from '../user-color'
@@ -54,6 +55,11 @@ export function StudioProvider({
unstable_history: history,
unstable_noAuthBoundary: noAuthBoundary,
}: StudioProviderProps) {
+ // We initialize the error reporter as early as possible in order to catch anything that could
+ // occur during configuration loading, React rendering etc. StudioProvider is often the highest
+ // mounted React component that is shared across embedded and standalone studios.
+ errorReporter.initialize()
+
const _children = (
diff --git a/packages/sanity/src/core/version.ts b/packages/sanity/src/core/version.ts
index 9f8aa81b747..95c63ca49b4 100644
--- a/packages/sanity/src/core/version.ts
+++ b/packages/sanity/src/core/version.ts
@@ -1,6 +1,8 @@
+import {version} from '../../package.json'
+
/**
* This version is provided by `@sanity/pkg-utils` at build time
* @hidden
* @beta
*/
-export const SANITY_VERSION = process.env.PKG_VERSION || ('0.0.0-development' as string)
+export const SANITY_VERSION = process.env.PKG_VERSION || `${version}-development`
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6e1fd2800b8..d68fcdea067 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1510,7 +1510,7 @@ importers:
version: 1.3.0
'@sanity/bifur-client':
specifier: ^0.4.0
- version: 0.4.1
+ version: 0.4.0
'@sanity/block-tools':
specifier: 3.46.1
version: link:../@sanity/block-tools
@@ -1580,6 +1580,9 @@ importers:
'@sanity/uuid':
specifier: ^3.0.1
version: 3.0.2
+ '@sentry/react':
+ specifier: ^8.7.0
+ version: 8.9.2(react@18.3.1)
'@tanstack/react-table':
specifier: ^8.16.0
version: 8.16.0(react-dom@18.3.1)(react@18.3.1)
@@ -6712,8 +6715,8 @@ packages:
- react-is
dev: false
- /@sanity/bifur-client@0.4.1:
- resolution: {integrity: sha512-mHM8WR7pujbIw2qxuV0lzinS1izOoyLza/ejWV6quITTLpBhUoPIQGPER3Ar0SON5JV0VEEqkJGa1kjiYYgx2w==}
+ /@sanity/bifur-client@0.4.0:
+ resolution: {integrity: sha512-5aXovw6//IGF/xOFl4q9hoq5kwHzYH1eJ88IS0AwPZEHmGEj8nuaWVu5SWUUOLYTMph+bVqHaDjB33MhWJ3hzQ==}
dependencies:
nanoid: 3.3.7
rxjs: 7.8.1
@@ -7458,6 +7461,91 @@ packages:
valibot: 0.31.1
dev: false
+ /@sentry-internal/browser-utils@8.9.2:
+ resolution: {integrity: sha512-2A0A6TnfzFDvYCRWS9My3t+JKG6KlslhyaN35BTiOTlYDauEekyJP7BFFyeTJXCHm2BQgI8aRZhBKm+oR9QuYw==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry/core': 8.9.2
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ dev: false
+
+ /@sentry-internal/feedback@8.9.2:
+ resolution: {integrity: sha512-v04Q+08ohwautwmiDfK5hI+nFW2B/IYhBz7pZM9x1srkwmNA69XOFyo5u34TeVHhYOPbMM2Ubs0uNEcSWHgbbQ==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry/core': 8.9.2
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ dev: false
+
+ /@sentry-internal/replay-canvas@8.9.2:
+ resolution: {integrity: sha512-vu9TssSjO+XbZjnoyYxMrBI4KgXG+zyqw3ThfPqG6o7O0BGa54fFwtZiMdGq/BHz017FuNiEz4fgtzuDd4gZJQ==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry-internal/replay': 8.9.2
+ '@sentry/core': 8.9.2
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ dev: false
+
+ /@sentry-internal/replay@8.9.2:
+ resolution: {integrity: sha512-YPnrnXJd6mJpJspJ8pI8hd1KTMOxw+BARP5twiDwXlij1RTotwnNoX9UGaSm+ZPTexPD++6Zyp6xQf4vKKP3yg==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry-internal/browser-utils': 8.9.2
+ '@sentry/core': 8.9.2
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ dev: false
+
+ /@sentry/browser@8.9.2:
+ resolution: {integrity: sha512-jI5XY4j8Sa+YteokI+4SW+A/ErZxPDnspjvV3dm5pIPWvEFhvDyXWZSepqaoqwo3L7fdkRMzXY8Bi4T7qDVMWg==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry-internal/browser-utils': 8.9.2
+ '@sentry-internal/feedback': 8.9.2
+ '@sentry-internal/replay': 8.9.2
+ '@sentry-internal/replay-canvas': 8.9.2
+ '@sentry/core': 8.9.2
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ dev: false
+
+ /@sentry/core@8.9.2:
+ resolution: {integrity: sha512-ixm8NISFlPlEo3FjSaqmq4nnd13BRHoafwJ5MG+okCz6BKGZ1SexEggP42/QpGvDprUUHnfncG6WUMgcarr1zA==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ dev: false
+
+ /@sentry/react@8.9.2(react@18.3.1):
+ resolution: {integrity: sha512-RK4tnkmGg1U9bAjMkY7iyKvZf1diGHYi5o8eOIrJ29OTg3c73C3/MyEuqAlP386tLglcQBn22u9JeP6g4yfiFg==}
+ engines: {node: '>=14.18'}
+ peerDependencies:
+ react: '*'
+ dependencies:
+ '@sentry/browser': 8.9.2
+ '@sentry/core': 8.9.2
+ '@sentry/types': 8.9.2
+ '@sentry/utils': 8.9.2
+ hoist-non-react-statics: 3.3.2
+ react: 18.3.1
+ dev: false
+
+ /@sentry/types@8.9.2:
+ resolution: {integrity: sha512-+LFOyQGl+zk5SZRGZD2MEURf7i5RHgP/mt3s85Rza+vz8M211WJ0YsjkIGUJFSY842nged5QLx4JysLaBlLymg==}
+ engines: {node: '>=14.18'}
+ dev: false
+
+ /@sentry/utils@8.9.2:
+ resolution: {integrity: sha512-A4srR9mEBFdVXwSEKjQ94msUbVkMr8JeFiEj9ouOFORw/Y/ux/WV2bWVD/ZI9wq0TcTNK8L1wBgU8UMS5lIq3A==}
+ engines: {node: '>=14.18'}
+ dependencies:
+ '@sentry/types': 8.9.2
+ dev: false
+
/@sideway/address@4.1.5:
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
dependencies: