0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Added Sentry integration to Admin X (#18288)

no issue

- Adds Sentry to Admin X settings to be able to monitor uncaught errors, etc.

---

<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at 76de125</samp>

Added Sentry integration to the admin-x-settings app to improve error
monitoring and reporting. Updated the `ErrorBoundary` and `HtmlEditor`
components, the `useFetchApi` hook, and the app component to use the
`@sentry/react` module. Passed the `sentryDSN` prop from the parent
component to the app component and the services context.
This commit is contained in:
Ronald Langeveld 2023-09-22 16:17:04 +07:00 committed by GitHub
parent 2bc1392bff
commit f7b50456ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 92 additions and 51 deletions

View file

@ -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",

View file

@ -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 (
<ErrorBoundary>
{children}
</ErrorBoundary>
);
}
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 (
<QueryClientProvider client={queryClient}>
<ServicesProvider ghostVersion={ghostVersion} officialThemes={officialThemes} toggleFeatureFlag={toggleFeatureFlag} unsplashConfig={unsplashConfig} zapierTemplates={zapierTemplates}>
<GlobalDataProvider>
<RoutingProvider externalNavigate={externalNavigate}>
<GlobalDirtyStateProvider>
<div className={appClassName} id="admin-x-root" style={{
height: '100vh',
width: '100%'
}}
>
<Toaster />
<NiceModal.Provider>
<MainContent />
</NiceModal.Provider>
</div>
</GlobalDirtyStateProvider>
</RoutingProvider>
</GlobalDataProvider>
</ServicesProvider>
</QueryClientProvider>
<SentryErrorBoundary>
<QueryClientProvider client={queryClient}>
<ServicesProvider ghostVersion={ghostVersion} officialThemes={officialThemes} sentryDSN={sentryDSN} toggleFeatureFlag={toggleFeatureFlag} unsplashConfig={unsplashConfig} zapierTemplates={zapierTemplates}>
<GlobalDataProvider>
<RoutingProvider externalNavigate={externalNavigate}>
<GlobalDirtyStateProvider>
<div className={appClassName} id="admin-x-root" style={{
height: '100vh',
width: '100%'
}}
>
<Toaster />
<NiceModal.Provider>
<MainContent />
</NiceModal.Provider>
</div>
</GlobalDirtyStateProvider>
</RoutingProvider>
</GlobalDataProvider>
</ServicesProvider>
</QueryClientProvider>
</SentryErrorBoundary>
);
}

View file

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

View file

@ -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<HtmlEditorProps & { editor: EditorResource }> = ({
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

View file

@ -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<ServicesContextProps>({
@ -42,10 +44,11 @@ const ServicesContext = createContext<ServicesContextProps>({
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': true
}
},
sentryDSN: null
});
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, zapierTemplates, officialThemes, toggleFeatureFlag, unsplashConfig}) => {
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, zapierTemplates, officialThemes, toggleFeatureFlag, unsplashConfig, sentryDSN}) => {
const search = useSearchService();
return (
@ -55,7 +58,8 @@ const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersi
zapierTemplates,
search,
unsplashConfig,
toggleFeatureFlag
toggleFeatureFlag,
sentryDSN
}}>
{children}
</ServicesContext.Provider>
@ -69,3 +73,5 @@ export const useServices = () => useContext(ServicesContext);
export const useOfficialThemes = () => useServices().officialThemes;
export const useSearch = () => useServices().search;
export const useSentryDSN = () => useServices().sentryDSN;

View file

@ -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={[]}

View file

@ -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();

View file

@ -325,6 +325,7 @@ export default class AdminXSettings extends Component {
toggleFeatureFlag={this.toggleFeatureFlag}
darkMode={this.feature.nightShift}
unsplashConfig={defaultUnsplashHeaders}
sentryDSN={this.config.sentry_dsn}
/>
</Suspense>
</ErrorHandler>

View file

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