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"