diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 6618554689..66285b7474 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -43,6 +43,7 @@ "@dnd-kit/core": "6.0.8", "@dnd-kit/sortable": "7.0.2", "@ebay/nice-modal-react": "1.2.10", + "@sentry/react": "7.70.0", "@tanstack/react-query": "4.35.3", "@tryghost/color-utils": "0.1.24", "@tryghost/limit-service": "^1.2.10", diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx index 2ad290b758..e433891e2a 100644 --- a/apps/admin-x-settings/src/App.tsx +++ b/apps/admin-x-settings/src/App.tsx @@ -1,14 +1,17 @@ +import * as Sentry from '@sentry/react'; import GlobalDataProvider from './components/providers/GlobalDataProvider'; import MainContent from './MainContent'; import NiceModal from '@ebay/nice-modal-react'; import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider'; import clsx from 'clsx'; import {DefaultHeaderTypes} from './utils/unsplash/UnsplashTypes'; +import {ErrorBoundary} from '@sentry/react'; import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState'; import {OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {Toaster} from 'react-hot-toast'; import {ZapierTemplate} from './components/settings/advanced/integrations/ZapierModal'; +import {useEffect} from 'react'; interface AppProps { ghostVersion: string; @@ -18,6 +21,7 @@ interface AppProps { toggleFeatureFlag: (flag: string, enabled: boolean) => void; darkMode?: boolean; unsplashConfig: DefaultHeaderTypes + sentryDSN: string | null; } const queryClient = new QueryClient({ @@ -30,33 +34,56 @@ const queryClient = new QueryClient({ } }); -function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, toggleFeatureFlag, darkMode = false, unsplashConfig}: AppProps) { +function SentryErrorBoundary({children}: {children: React.ReactNode}) { + return ( + + {children} + + ); +} + +function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, toggleFeatureFlag, darkMode = false, unsplashConfig, sentryDSN}: AppProps) { const appClassName = clsx( 'admin-x-settings h-[100vh] w-full overflow-y-auto overflow-x-hidden', darkMode && 'dark' ); + + useEffect(() => { + if (sentryDSN) { + Sentry.init({ + dsn: sentryDSN, + release: ghostVersion, + integrations: [ + new Sentry.BrowserTracing({ + }) + ] + }); + } + }, [sentryDSN, ghostVersion]); return ( - - - - - -
- - - - -
-
-
-
-
-
+ + + + + + +
+ + + + +
+
+
+
+
+
+
); } diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx b/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx index bcb13504be..4fe0db91ad 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react'; import Banner from './Banner'; import React, {ComponentType, ErrorInfo, ReactNode} from 'react'; @@ -17,7 +18,10 @@ class ErrorBoundary extends React.Component<{children: ReactNode, name: ReactNod } componentDidCatch(error: unknown, info: ErrorInfo) { - // TODO: Log to Sentry + Sentry.withScope((scope) => { + scope.setTag('adminX settings component-', info.componentStack); + Sentry.captureException(error); + }); // eslint-disable-next-line no-console console.error(error); // eslint-disable-next-line no-console diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx index 030d5f9ac3..5ce6b5ca85 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react'; import React, {ReactNode, Suspense, useCallback, useMemo} from 'react'; export interface HtmlEditorProps { @@ -90,21 +91,21 @@ const KoenigWrapper: React.FC = ({ nodes }) => { const onError = useCallback((error: unknown) => { - // ensure we're still showing errors in development + try { + Sentry.captureException({ + error, + tags: {lexical: true}, + contexts: { + koenig: { + version: window['@tryghost/koenig-lexical']?.version + } + } + }); + } catch (e) { + // if this fails, Sentry is probably not initialized + console.error(e); // eslint-disable-line + } console.error(error); // eslint-disable-line - - // Pass down Sentry from Ember? - // if (this.config.sentry_dsn) { - // Sentry.captureException(error, { - // tags: {lexical: true}, - // contexts: { - // koenig: { - // version: window['@tryghost/koenig-lexical']?.version - // } - // } - // }); - // } - // don't rethrow, Lexical will attempt to gracefully recover }, []); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx b/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx index 122b121fd8..19a10b2f1f 100644 --- a/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx +++ b/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx @@ -19,6 +19,7 @@ interface ServicesContextProps { search: SearchService; unsplashConfig: DefaultHeaderTypes; toggleFeatureFlag: (flag: string, enabled: boolean) => void; + sentryDSN: string | null; } interface ServicesProviderProps { @@ -28,6 +29,7 @@ interface ServicesProviderProps { officialThemes: OfficialTheme[]; toggleFeatureFlag: (flag: string, enabled: boolean) => void; unsplashConfig: DefaultHeaderTypes; + sentryDSN: string | null; } const ServicesContext = createContext({ @@ -42,10 +44,11 @@ const ServicesContext = createContext({ 'Content-Type': '', 'App-Pragma': '', 'X-Unsplash-Cache': true - } + }, + sentryDSN: null }); -const ServicesProvider: React.FC = ({children, ghostVersion, zapierTemplates, officialThemes, toggleFeatureFlag, unsplashConfig}) => { +const ServicesProvider: React.FC = ({children, ghostVersion, zapierTemplates, officialThemes, toggleFeatureFlag, unsplashConfig, sentryDSN}) => { const search = useSearchService(); return ( @@ -55,7 +58,8 @@ const ServicesProvider: React.FC = ({children, ghostVersi zapierTemplates, search, unsplashConfig, - toggleFeatureFlag + toggleFeatureFlag, + sentryDSN }}> {children} @@ -69,3 +73,5 @@ export const useServices = () => useContext(ServicesContext); export const useOfficialThemes = () => useServices().officialThemes; export const useSearch = () => useServices().search; + +export const useSentryDSN = () => useServices().sentryDSN; diff --git a/apps/admin-x-settings/src/main.tsx b/apps/admin-x-settings/src/main.tsx index 5be3aaeb0b..66fc1a3327 100644 --- a/apps/admin-x-settings/src/main.tsx +++ b/apps/admin-x-settings/src/main.tsx @@ -30,6 +30,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ref: 'TryGhost/Edition', image: 'assets/img/themes/Edition.png' }]} + sentryDSN={'' as string | null} toggleFeatureFlag={() => {}} unsplashConfig={{} as DefaultHeaderTypes} zapierTemplates={[]} diff --git a/apps/admin-x-settings/src/utils/apiRequests.ts b/apps/admin-x-settings/src/utils/apiRequests.ts index ac8bd51f72..02e85b6865 100644 --- a/apps/admin-x-settings/src/utils/apiRequests.ts +++ b/apps/admin-x-settings/src/utils/apiRequests.ts @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react'; import handleError from './handleError'; import handleResponse from './handleResponse'; import {APIError, MaintenanceError, ServerUnreachableError, TimeoutError} from './errors'; @@ -5,7 +6,7 @@ import {QueryClient, UseInfiniteQueryOptions, UseQueryOptions, useInfiniteQuery, import {getGhostPaths} from './helpers'; import {useEffect, useMemo} from 'react'; import {usePage, usePagination} from '../hooks/usePagination'; -import {useServices} from '../components/providers/ServiceProvider'; +import {useSentryDSN, useServices} from '../components/providers/ServiceProvider'; export interface Meta { pagination: { @@ -30,6 +31,7 @@ interface RequestOptions { export const useFetchApi = () => { const {ghostVersion} = useServices(); + const sentrydsn = useSentryDSN(); return async (endpoint: string | URL, options: RequestOptions = {}) => { // By default, we set the Content-Type header to application/json @@ -85,10 +87,9 @@ export const useFetchApi = () => { ...options }); - // TODO: Add Sentry integration - // if (attempts !== 0 && config.sentry_dsn) { - // captureMessage('Request took multiple attempts', {extra: getErrorData()}); - // } + if (attempts !== 0 && sentrydsn) { + Sentry.captureMessage('Request took multiple attempts', {extra: {attempts, retryingMs, endpoint: endpoint.toString()}}); + } return handleResponse(response); } catch (error) { @@ -102,10 +103,9 @@ export const useFetchApi = () => { continue; } - // TODO: Add Sentry integration - // if (attempts > 0 && config.sentry_dsn) { - // captureMessage('Request failed after multiple attempts', {extra: getErrorData()}); - // } + if (attempts !== 0 && sentrydsn) { + Sentry.captureMessage('Request failed after multiple attempts', {extra: {attempts, retryingMs, endpoint: endpoint.toString()}}); + } if (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') { throw new TimeoutError(); diff --git a/ghost/admin/app/components/admin-x/settings.js b/ghost/admin/app/components/admin-x/settings.js index 22c39a000a..185d62492d 100644 --- a/ghost/admin/app/components/admin-x/settings.js +++ b/ghost/admin/app/components/admin-x/settings.js @@ -325,6 +325,7 @@ export default class AdminXSettings extends Component { toggleFeatureFlag={this.toggleFeatureFlag} darkMode={this.feature.nightShift} unsplashConfig={defaultUnsplashHeaders} + sentryDSN={this.config.sentry_dsn} /> diff --git a/yarn.lock b/yarn.lock index afa5ab8de5..2049251e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2637,9 +2637,9 @@ tslib "^2.4.0" "@elastic/transport@^8.2.0": - version "8.3.3" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.3.tgz#06c5b1b9566796775ac96d17959dafc269da5ec1" - integrity sha512-g5nc//dq/RQUTMkJUB8Ui8KJa/WflWmUa7yLl4SRZd67PPxIp3cn+OvGMNIhpiLRcfz1upanzgZHb/7Po2eEdQ== + version "8.3.4" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.4.tgz#43c852e848dc8502bbd7f23f2d61bd5665cded99" + integrity sha512-+0o8o74sbzu3BO7oOZiP9ycjzzdOt4QwmMEjFc1zfO7M0Fh7QX1xrpKqZbSd8vBwihXNlSq/EnMPfgD2uFEmFg== dependencies: debug "^4.3.4" hpagent "^1.0.0"