diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index d7e297a97e0..f653bec807a 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -12,6 +12,7 @@ export { featureEnabled } from './utils/licensing'; export { logInfo, logDebug, logWarning, logError } from './utils/logging'; export { DataSourceWithBackend, + HealthCheckError, HealthCheckResult, HealthCheckResultDetails, HealthStatus, diff --git a/packages/grafana-runtime/src/services/backendSrv.ts b/packages/grafana-runtime/src/services/backendSrv.ts index 4260ef410de..b5bdd774a2f 100644 --- a/packages/grafana-runtime/src/services/backendSrv.ts +++ b/packages/grafana-runtime/src/services/backendSrv.ts @@ -123,6 +123,10 @@ export interface FetchError { config: BackendSrvRequest; } +export function isFetchError(e: unknown): e is FetchError { + return typeof e === 'object' && e !== null && 'status' in e && 'data' in e; +} + /** * Used to communicate via http(s) to a remote backend such as the Grafana backend, * a datasource etc. The BackendSrv is using the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API} diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 5bbc93b4a84..81db268ade3 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -48,7 +48,7 @@ export function isExpressionReference(ref?: DataSourceRef | string | null): bool return v === ExpressionDatasourceRef.type || v === '-100'; // -100 was a legacy accident that should be removed } -class HealthCheckError extends Error { +export class HealthCheckError extends Error { details: HealthCheckResultDetails; constructor(message: string, details: HealthCheckResultDetails) { diff --git a/public/app/core/components/RolePicker/api.ts b/public/app/core/components/RolePicker/api.ts index 9f07f61a724..82549055a0c 100644 --- a/public/app/core/components/RolePicker/api.ts +++ b/public/app/core/components/RolePicker/api.ts @@ -1,4 +1,4 @@ -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { Role } from 'app/types'; export const fetchRoleOptions = async (orgId?: number, query?: string): Promise => { @@ -33,7 +33,9 @@ export const fetchUserRoles = async (userId: number, orgId?: number): Promise { cancel: () => void; } +export interface CancelablePromiseRejection { + isCanceled: boolean; +} + +export function isCancelablePromiseRejection(promise: unknown): promise is CancelablePromiseRejection { + return typeof promise === 'object' && promise !== null && 'isCanceled' in promise; +} + export const makePromiseCancelable = (promise: Promise): CancelablePromise => { let hasCanceled_ = false; const wrappedPromise = new Promise((resolve, reject) => { - promise.then((val) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val))); - promise.catch((error) => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))); + const canceledPromiseRejection: CancelablePromiseRejection = { isCanceled: true }; + promise.then((val) => (hasCanceled_ ? reject(canceledPromiseRejection) : resolve(val))); + promise.catch((error) => (hasCanceled_ ? reject(canceledPromiseRejection) : reject(error))); }); return { diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index 560aa4a95a5..c2c34ef50e3 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -58,11 +58,13 @@ export async function addToRichHistory( }); warning = result.warning; } catch (error) { - if (error.name === RichHistoryServiceError.StorageFull) { - richHistoryStorageFull = true; - showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message))); - } else if (error.name !== RichHistoryServiceError.DuplicatedEntry) { - dispatch(notifyApp(createErrorNotification('Rich History update failed', error.message))); + if (error instanceof Error) { + if (error.name === RichHistoryServiceError.StorageFull) { + richHistoryStorageFull = true; + showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message))); + } else if (error.name !== RichHistoryServiceError.DuplicatedEntry) { + dispatch(notifyApp(createErrorNotification('Rich History update failed', error.message))); + } } // Saving failed. Do not add new entry. return { richHistoryStorageFull, limitExceeded }; @@ -101,7 +103,9 @@ export async function updateStarredInRichHistory(id: string, starred: boolean) { try { return await getRichHistoryStorage().updateStarred(id, starred); } catch (error) { - dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message))); + if (error instanceof Error) { + dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message))); + } return undefined; } } @@ -110,7 +114,9 @@ export async function updateCommentInRichHistory(id: string, newComment: string try { return await getRichHistoryStorage().updateComment(id, newComment); } catch (error) { - dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message))); + if (error instanceof Error) { + dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message))); + } return undefined; } } @@ -120,7 +126,9 @@ export async function deleteQueryInRichHistory(id: string) { await getRichHistoryStorage().deleteRichHistory(id); return id; } catch (error) { - dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message))); + if (error instanceof Error) { + dispatch(notifyApp(createErrorNotification('Saving rich history failed', error.message))); + } return undefined; } } @@ -156,8 +164,9 @@ export async function migrateQueryHistoryFromLocalStorage(): Promise { } catch (error) { console.error(error); - const userError = { - title: error.data.message, - body: error.data.error, - }; + if (isFetchError(error)) { + const userError = { + title: error.data.message, + body: error.data.error, + }; - dispatch(userAdminPageFailedAction(userError)); + dispatch(userAdminPageFailedAction(userError)); + } } }; } @@ -212,12 +214,14 @@ export function loadLdapState(): ThunkResult { const connectionInfo = await getBackendSrv().get(`/api/admin/ldap/status`); dispatch(ldapConnectionInfoLoadedAction(connectionInfo)); } catch (error) { - error.isHandled = true; - const ldapError = { - title: error.data.message, - body: error.data.error, - }; - dispatch(ldapFailedAction(ldapError)); + if (isFetchError(error)) { + error.isHandled = true; + const ldapError = { + title: error.data.message, + body: error.data.error, + }; + dispatch(ldapFailedAction(ldapError)); + } } }; } @@ -235,13 +239,15 @@ export function loadUserMapping(username: string): ThunkResult { }; dispatch(userMappingInfoLoadedAction(userInfo)); } catch (error) { - error.isHandled = true; - const userError = { - title: error.data.message, - body: error.data.error, - }; - dispatch(clearUserMappingInfoAction()); - dispatch(userMappingInfoFailedAction(userError)); + if (isFetchError(error)) { + error.isHandled = true; + const userError = { + title: error.data.message, + body: error.data.error, + }; + dispatch(clearUserMappingInfoAction()); + dispatch(userMappingInfoFailedAction(userError)); + } } }; } diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 11634d3620a..84f5ac3c86e 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,4 +1,4 @@ -import { getBackendSrv, locationService } from '@grafana/runtime'; +import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types'; @@ -28,7 +28,9 @@ export function createNotificationChannel(data: any): ThunkResult> dispatch(notifyApp(createSuccessNotification('Notification created'))); locationService.push('/alerting/notifications'); } catch (error) { - dispatch(notifyApp(createErrorNotification(error.data.error))); + if (isFetchError(error)) { + dispatch(notifyApp(createErrorNotification(error.data.error))); + } } }; } @@ -39,7 +41,9 @@ export function updateNotificationChannel(data: any): ThunkResult { await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data); dispatch(notifyApp(createSuccessNotification('Notification updated'))); } catch (error) { - dispatch(notifyApp(createErrorNotification(error.data.error))); + if (isFetchError(error)) { + dispatch(notifyApp(createErrorNotification(error.data.error))); + } } }; } diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 28649614ab8..78a4385bdc1 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -1,7 +1,7 @@ import { lastValueFrom } from 'rxjs'; import { urlUtil } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { AlertmanagerAlert, AlertManagerCortexConfig, @@ -18,7 +18,6 @@ import { ExternalAlertmanagerConfig, } from 'app/plugins/datasource/alertmanager/types'; -import { isFetchError } from '../utils/alertmanager'; import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; // "grafana" for grafana-managed, otherwise a datasource name @@ -39,6 +38,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P // if no config has been uploaded to grafana, it returns error instead of latest config if ( alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && + isFetchError(e) && e.data?.message?.includes('could not find an Alertmanager configuration') ) { return { diff --git a/public/app/features/alerting/unified/api/buildInfo.test.ts b/public/app/features/alerting/unified/api/buildInfo.test.ts index 1bbbafcc220..b01af7f763e 100644 --- a/public/app/features/alerting/unified/api/buildInfo.test.ts +++ b/public/app/features/alerting/unified/api/buildInfo.test.ts @@ -12,6 +12,7 @@ jest.mock('./prometheus'); jest.mock('./ruler'); jest.mock('app/core/services/context_srv', () => {}); jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ fetch }), })); diff --git a/public/app/features/alerting/unified/api/buildInfo.ts b/public/app/features/alerting/unified/api/buildInfo.ts index cc5a06039a1..cc623411e5e 100644 --- a/public/app/features/alerting/unified/api/buildInfo.ts +++ b/public/app/features/alerting/unified/api/buildInfo.ts @@ -1,9 +1,8 @@ import { lastValueFrom } from 'rxjs'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { PromApplication, PromApiFeatures, PromBuildInfoResponse } from 'app/types/unified-alerting-dto'; -import { isFetchError } from '../utils/alertmanager'; import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; import { getDataSourceByName } from '../utils/datasource'; diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx index fdf286df090..bdb3e0cc5cb 100644 --- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx @@ -112,7 +112,7 @@ export default function AlertmanagerConfig(): JSX.Element { JSON.parse(v); return true; } catch (e) { - return e.message; + return e instanceof Error ? e.message : 'Invalid JSON.'; } }, })} diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts index d569c32442a..90a892a4703 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.ts @@ -1,5 +1,4 @@ import { SelectableValue } from '@grafana/data'; -import { FetchError } from '@grafana/runtime'; import { AlertManagerCortexConfig, MatcherOperator, @@ -260,7 +259,3 @@ export function getMonthsString(months?: string[]): string { export function getYearsString(years?: string[]): string { return 'Years: ' + (years?.join(', ') ?? 'All'); } - -export function isFetchError(e: unknown): e is FetchError { - return typeof e === 'object' && e !== null && 'status' in e && 'data' in e; -} diff --git a/public/app/features/alerting/unified/utils/redux.ts b/public/app/features/alerting/unified/utils/redux.ts index d1eba541758..193d2a0774d 100644 --- a/public/app/features/alerting/unified/utils/redux.ts +++ b/public/app/features/alerting/unified/utils/redux.ts @@ -1,11 +1,9 @@ import { AsyncThunk, createSlice, Draft, isAsyncThunkAction, PayloadAction, SerializedError } from '@reduxjs/toolkit'; import { AppEvents } from '@grafana/data'; -import { FetchError } from '@grafana/runtime'; +import { FetchError, isFetchError } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; -import { isFetchError } from './alertmanager'; - export interface AsyncRequestState { result?: T; loading: boolean; diff --git a/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx b/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx index fa9ec12888b..9a87edae572 100644 --- a/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx @@ -82,7 +82,7 @@ export const validateIntervals = ( getValidIntervals(intervals, dependencies); return null; } catch (err) { - return err.message; + return err instanceof Error ? err.message : 'Invalid intervals'; } }; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 8221172b50e..076ccffda0e 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -6,7 +6,7 @@ import { Subscription } from 'rxjs'; import { FieldConfigSource, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { locationService } from '@grafana/runtime'; +import { isFetchError, locationService } from '@grafana/runtime'; import { HorizontalGroup, InlineSwitch, @@ -163,7 +163,9 @@ export class PanelEditorUnconnected extends PureComponent { await saveAndRefreshLibraryPanel(this.props.panel, this.props.dashboard.meta.folderId!); this.props.notifyApp(createPanelLibrarySuccessNotification('Library panel saved')); } catch (err) { - this.props.notifyApp(createPanelLibraryErrorNotification(`Error saving library panel: "${err.statusText}"`)); + if (isFetchError(err)) { + this.props.notifyApp(createPanelLibraryErrorNotification(`Error saving library panel: "${err.statusText}"`)); + } } return; } diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.tsx index b1176a0808e..283ea733e9d 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.tsx @@ -64,7 +64,7 @@ export const SaveDashboardAsForm: React.FC = ({ await validationSrv.validateNewDashboardName(getFormValues().$folder.id, dashboardName); return true; } catch (e) { - return e.message; + return e instanceof Error ? e.message : 'Dashboard name is invalid'; } }; diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts index eae72482c81..219d67bb011 100644 --- a/public/app/features/dashboard/state/initDashboard.test.ts +++ b/public/app/features/dashboard/state/initDashboard.test.ts @@ -2,7 +2,7 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { Subject } from 'rxjs'; -import { locationService, setEchoSrv } from '@grafana/runtime'; +import { FetchError, locationService, setEchoSrv } from '@grafana/runtime'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { keybindingSrv } from 'app/core/services/keybindingSrv'; import { variableAdapters } from 'app/features/variables/adapters'; @@ -208,7 +208,15 @@ describeInitScenario('Initializing home dashboard', (ctx) => { describeInitScenario('Initializing home dashboard cancelled', (ctx) => { ctx.setup(() => { ctx.args.routeName = DashboardRoutes.Home; - ctx.backendSrv.get.mockRejectedValue({ cancelled: true }); + const fetchError: FetchError = { + cancelled: true, + config: { + url: '/api/dashboards/home', + }, + data: 'foo', + status: 500, + }; + ctx.backendSrv.get.mockRejectedValue(fetchError); }); it('Should abort init process', () => { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 396148797e6..ddd7fdc7dc2 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,5 +1,5 @@ import { locationUtil, setWeekStart } from '@grafana/data'; -import { config, locationService } from '@grafana/runtime'; +import { config, isFetchError, locationService } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { backendSrv } from 'app/core/services/backend_srv'; @@ -90,7 +90,7 @@ async function fetchDashboard( } } catch (err) { // Ignore cancelled errors - if (err.cancelled) { + if (isFetchError(err) && err.cancelled) { return null; } @@ -184,7 +184,9 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult { keybindingSrv.setupDashboardBindings(dashboard); } catch (err) { - dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); + if (err instanceof Error) { + dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); + } console.error(err); } diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index 84e6ca9c108..8425aeb836f 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -1,7 +1,7 @@ import { of } from 'rxjs'; import { thunkTester } from 'test/core/thunk/thunkTester'; -import { BackendSrvRequest, FetchResponse } from '@grafana/runtime'; +import { BackendSrvRequest, FetchError, FetchResponse } from '@grafana/runtime'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { ThunkResult, ThunkDispatch } from 'app/types'; @@ -316,13 +316,17 @@ describe('testDataSource', () => { it('then testDataSourceFailed should be dispatched with response error message', async () => { const result = { - message: 'Error testing datasource', + message: 'Response error message', }; - const dispatchedActions = await failDataSourceTest({ - message: 'Error testing datasource', + const error: FetchError = { + config: { + url: '', + }, data: { message: 'Response error message' }, + status: 400, statusText: 'Bad Request', - }); + }; + const dispatchedActions = await failDataSourceTest(error); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); }); @@ -330,10 +334,15 @@ describe('testDataSource', () => { const result = { message: 'Response error message', }; - const dispatchedActions = await failDataSourceTest({ + const error: FetchError = { + config: { + url: '', + }, data: { message: 'Response error message' }, + status: 400, statusText: 'Bad Request', - }); + }; + const dispatchedActions = await failDataSourceTest(error); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); }); @@ -341,7 +350,15 @@ describe('testDataSource', () => { const result = { message: 'HTTP error Bad Request', }; - const dispatchedActions = await failDataSourceTest({ data: {}, statusText: 'Bad Request' }); + const error: FetchError = { + config: { + url: '', + }, + data: {}, + statusText: 'Bad Request', + status: 400, + }; + const dispatchedActions = await failDataSourceTest(error); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); }); }); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 9dd8d5d26e9..a95d07cb030 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -1,7 +1,14 @@ import { lastValueFrom } from 'rxjs'; import { DataSourcePluginMeta, DataSourceSettings, locationUtil } from '@grafana/data'; -import { DataSourceWithBackend, getDataSourceSrv, locationService } from '@grafana/runtime'; +import { + DataSourceWithBackend, + getDataSourceSrv, + HealthCheckError, + HealthCheckResultDetails, + isFetchError, + locationService, +} from '@grafana/runtime'; import { updateNavIndex } from 'app/core/actions'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; @@ -77,7 +84,9 @@ export const initDataSourceSettings = ( dispatch(initDataSourceSettingsSucceeded(importedPlugin)); } catch (err) { - dispatch(initDataSourceSettingsFailed(err)); + if (err instanceof Error) { + dispatch(initDataSourceSettingsFailed(err)); + } } }; }; @@ -104,9 +113,17 @@ export const testDataSource = ( dispatch(testDataSourceSucceeded(result)); } catch (err) { - const { statusText, message: errMessage, details, data } = err; + let message: string | undefined; + let details: HealthCheckResultDetails; - const message = errMessage || data?.message || 'HTTP error ' + statusText; + if (err instanceof HealthCheckError) { + message = err.message; + details = err.details; + } else if (isFetchError(err)) { + message = err.data.message ?? `HTTP error ${err.statusText}`; + } else if (err instanceof Error) { + message = err.message; + } dispatch(testDataSourceFailed({ message, details })); } diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 1a754a5a8c7..a1abd069fe7 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,7 +1,7 @@ import { lastValueFrom } from 'rxjs'; import { locationUtil } from '@grafana/data'; -import { getBackendSrv, locationService } from '@grafana/runtime'; +import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { notifyApp, updateNavIndex } from 'app/core/actions'; import { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; @@ -59,7 +59,7 @@ export function checkFolderPermissions(uid: string): ThunkResult { ); dispatch(setCanViewFolderPermissions(true)); } catch (err) { - if (err.status !== 403) { + if (isFetchError(err) && err.status !== 403) { dispatch(notifyApp(createWarningNotification('Error checking folder permissions', err.data?.message))); } diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index 7173d22125f..3aee558d73f 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useAsync, useDebounce } from 'react-use'; +import { isFetchError } from '@grafana/runtime'; import { Button, Field, Input, Modal } from '@grafana/ui'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; @@ -36,7 +37,9 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A try { return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderId === folderId); } catch (err) { - err.isHandled = true; + if (isFetchError(err)) { + err.isHandled = true; + } return true; } finally { setWaiting(false); diff --git a/public/app/features/library-panels/utils/usePanelSave.ts b/public/app/features/library-panels/utils/usePanelSave.ts index 90bb0e468a6..d20fdc01f3e 100644 --- a/public/app/features/library-panels/utils/usePanelSave.ts +++ b/public/app/features/library-panels/utils/usePanelSave.ts @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { isFetchError } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { PanelModel } from 'app/features/dashboard/state'; @@ -17,8 +18,11 @@ export const usePanelSave = () => { try { return await saveAndRefreshLibraryPanel(panel, folderId); } catch (err) { - err.isHandled = true; - throw new Error(err.data.message); + if (isFetchError(err)) { + err.isHandled = true; + throw new Error(err.data.message); + } + throw err; } }, []); diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index 9ea96938469..1085441536a 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -84,10 +84,12 @@ class UnthemedDashboardImport extends PureComponent { try { dashboard = JSON.parse(e.target.result); } catch (error) { - appEvents.emit(AppEvents.alertError, [ - 'Import failed', - 'JSON -> JS Serialization failed: ' + error.message, - ]); + if (error instanceof Error) { + appEvents.emit(AppEvents.alertError, [ + 'Import failed', + 'JSON -> JS Serialization failed: ' + error.message, + ]); + } return; } importDashboardJson(dashboard); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index e5d23f4c87b..23d0b18527e 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,5 +1,5 @@ import { DataSourceInstanceSettings, locationUtil } from '@grafana/data'; -import { getDataSourceSrv, locationService, getBackendSrv } from '@grafana/runtime'; +import { getDataSourceSrv, locationService, getBackendSrv, isFetchError } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; @@ -34,7 +34,9 @@ export function fetchGcomDashboard(id: string): ThunkResult { dispatch(processElements(dashboard.json)); } catch (error) { dispatch(fetchFailed()); - dispatch(notifyApp(createErrorNotification(error.data.message || error))); + if (isFetchError(error)) { + dispatch(notifyApp(createErrorNotification(error.data.message || error))); + } } }; } @@ -213,11 +215,13 @@ async function moveDashboard(uid: string, toFolder: FolderInfo) { await saveDashboard(options); return { succeeded: true }; } catch (err) { - if (err.data?.status !== 'plugin-dashboard') { - return { succeeded: false }; - } + if (isFetchError(err)) { + if (err.data?.status !== 'plugin-dashboard') { + return { succeeded: false }; + } - err.isHandled = true; + err.isHandled = true; + } options.overwrite = true; try { diff --git a/public/app/features/playlist/api.ts b/public/app/features/playlist/api.ts index 4474d8970e3..570e4a65171 100644 --- a/public/app/features/playlist/api.ts +++ b/public/app/features/playlist/api.ts @@ -33,6 +33,8 @@ async function withErrorHandling(apiCall: () => Promise, message = 'Playli await apiCall(); dispatch(notifyApp(createSuccessNotification(message))); } catch (e) { - dispatch(notifyApp(createErrorNotification('Unable to save playlist', e))); + if (e instanceof Error) { + dispatch(notifyApp(createErrorNotification('Unable to save playlist', e))); + } } } diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 949209b42bc..5aa840276ae 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -1,5 +1,5 @@ import { PluginError, PluginMeta, renderMarkdown } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { API_ROOT, GCOM_API_ROOT } from './constants'; import { isLocalPluginVisible, isRemotePluginVisible } from './helpers'; @@ -45,8 +45,10 @@ async function getRemotePlugin(id: string): Promise { try { return await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}`, {}); } catch (error) { - // It can happen that GCOM is not available, in that case we show a limited set of information to the user. - error.isHandled = true; + if (isFetchError(error)) { + // It can happen that GCOM is not available, in that case we show a limited set of information to the user. + error.isHandled = true; + } return; } } @@ -66,8 +68,10 @@ async function getPluginVersions(id: string, isPublished: boolean): Promise { return markdownAsHtml; } catch (error) { - error.isHandled = true; + if (isFetchError(error)) { + error.isHandled = true; + } return ''; } } diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index 408f1d38833..767d2f27b63 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -1,7 +1,7 @@ import { createAction, createAsyncThunk, Update } from '@reduxjs/toolkit'; import { PanelPlugin } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; import { StoreState, ThunkResult } from 'app/types'; @@ -39,7 +39,9 @@ export const fetchRemotePlugins = createAsyncThunk { silenceConsoleOutput(); it('then correct actions are dispatched', async () => { const variable = createVariable({ includeAll: true }); - const error = { message: 'failed to fetch metrics' }; + const error = new Error('failed to fetch metrics'); mocks[variable.datasource!.uid!].metricFindQuery = jest.fn(() => Promise.reject(error)); @@ -233,10 +233,7 @@ describe('query actions', () => { toKeyedAction('key', addVariableEditorError({ errorProp: 'update', errorText: error.message })) ); expect(dispatchedActions[3]).toEqual( - toKeyedAction( - 'key', - variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } })) - ) + toKeyedAction('key', variableStateFailed(toVariablePayload(variable, { error }))) ); expect(dispatchedActions[4].type).toEqual(notifyApp.type); expect(dispatchedActions[4].payload.title).toEqual('Templating [0]'); diff --git a/public/app/features/variables/query/actions.ts b/public/app/features/variables/query/actions.ts index f2aca6b6e59..a2c52ac487d 100644 --- a/public/app/features/variables/query/actions.ts +++ b/public/app/features/variables/query/actions.ts @@ -47,15 +47,17 @@ export const updateQueryVariableOptions = ( getVariableQueryRunner().queueRequest({ identifier, datasource, searchFilter }); }); } catch (err) { - const error = toDataQueryError(err); - const { rootStateKey } = identifier; - if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) { - dispatch( - toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message })) - ); - } + if (err instanceof Error) { + const error = toDataQueryError(err); + const { rootStateKey } = identifier; + if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) { + dispatch( + toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message })) + ); + } - throw error; + throw error; + } } }; }; diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index 5dc682a3a36..7d54e625375 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -116,7 +116,9 @@ function useTimoutValidation(value: string | undefined) { rangeUtil.describeInterval(value); setErr(undefined); } catch (e) { - setErr(e.toString()); + if (e instanceof Error) { + setErr(e.toString()); + } } } else { setErr(undefined); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index b9cf3a51899..f712f119ff8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -1,7 +1,7 @@ import { filter, find, startsWith } from 'lodash'; import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; -import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, isFetchError } from '@grafana/runtime'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { resourceTypeDisplayNames } from '../azureMetadata'; @@ -314,14 +314,18 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { this.setState({ error: undefined }); return ResponseParser.parseSubscriptionsForSelect(result); } catch (err) { - this.setState({ - error: { - title: 'Error requesting subscriptions', - description: 'Could not request subscriptions from Azure. Check your credentials and try again.', - details: err?.data?.message, - }, - }); + if (isFetchError(err)) { + this.setState({ + error: { + title: 'Error requesting subscriptions', + description: 'Could not request subscriptions from Azure. Check your credentials and try again.', + details: err?.data?.message, + }, + }); + } return Promise.resolve([]); } }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts index 49ac840360e..f41dff6c687 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts @@ -93,7 +93,11 @@ describe('AzureMonitor resourcePickerData', () => { await resourcePickerData.getSubscriptions(); throw Error('expected getSubscriptions to fail but it succeeded'); } catch (err) { - expect(err.message).toEqual('No subscriptions were found'); + if (err instanceof Error) { + expect(err.message).toEqual('No subscriptions were found'); + } else { + throw err; + } } }); }); @@ -178,7 +182,11 @@ describe('AzureMonitor resourcePickerData', () => { await resourcePickerData.getResourceGroupsBySubscriptionId('123'); throw Error('expected getResourceGroupsBySubscriptionId to fail but it succeeded'); } catch (err) { - expect(err.message).toEqual('unable to fetch resource groups'); + if (err instanceof Error) { + expect(err.message).toEqual('unable to fetch resource groups'); + } else { + throw err; + } } }); }); @@ -232,7 +240,11 @@ describe('AzureMonitor resourcePickerData', () => { await resourcePickerData.getResourcesForResourceGroup('dev'); throw Error('expected getResourcesForResourceGroup to fail but it succeeded'); } catch (err) { - expect(err.message).toEqual('unable to fetch resource details'); + if (err instanceof Error) { + expect(err.message).toEqual('unable to fetch resource details'); + } else { + throw err; + } } }); }); @@ -317,7 +329,11 @@ describe('AzureMonitor resourcePickerData', () => { await resourcePickerData.search('dev', 'logs'); throw Error('expected search test to fail but it succeeded'); } catch (err) { - expect(err.message).toEqual('unable to fetch resource details'); + if (err instanceof Error) { + expect(err.message).toEqual('unable to fetch resource details'); + } else { + throw err; + } } }); }); diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 40232d19dfe..ac38010a0f1 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -79,8 +79,10 @@ export default class GraphiteQuery { try { this.parseTargetRecursive(astNode, null); } catch (err) { - console.error('error parsing target:', err.message); - this.error = err.message; + if (err instanceof Error) { + console.error('error parsing target:', err.message); + this.error = err.message; + } this.target.textEditor = true; } diff --git a/public/app/plugins/datasource/graphite/parser.ts b/public/app/plugins/datasource/graphite/parser.ts index b276ee20325..8eee34554e1 100644 --- a/public/app/plugins/datasource/graphite/parser.ts +++ b/public/app/plugins/datasource/graphite/parser.ts @@ -1,4 +1,6 @@ import { Lexer } from './lexer'; +import { GraphiteParserError } from './types'; +import { isGraphiteParserError } from './utils'; export class Parser { expression: any; @@ -21,11 +23,13 @@ export class Parser { try { return this.functionCall() || this.metricExpression(); } catch (e) { - return { - type: 'error', - message: e.message, - pos: e.pos, - }; + if (isGraphiteParserError(e)) { + return { + type: 'error', + message: e.message, + pos: e.pos, + }; + } } } @@ -222,7 +226,11 @@ export class Parser { const token = this.consumeToken(); if (token.isUnclosed) { - throw { message: 'Unclosed string parameter', pos: token.pos }; + const error: GraphiteParserError = { + message: 'Unclosed string parameter', + pos: token.pos, + }; + throw error; } return { @@ -234,10 +242,11 @@ export class Parser { errorMark(text: string) { const currentToken = this.tokens[this.index]; const type = currentToken ? currentToken.type : 'end of string'; - throw { + const error: GraphiteParserError = { message: text + ' instead found ' + type, pos: currentToken ? currentToken.pos : this.lexer.char, }; + throw error; } // returns token value and incre diff --git a/public/app/plugins/datasource/graphite/state/helpers.ts b/public/app/plugins/datasource/graphite/state/helpers.ts index 3378edf16a2..0f58be18135 100644 --- a/public/app/plugins/datasource/graphite/state/helpers.ts +++ b/public/app/plugins/datasource/graphite/state/helpers.ts @@ -90,7 +90,9 @@ export async function checkOtherSegments( } } } catch (err) { - handleMetricsAutoCompleteError(state, err); + if (err instanceof Error) { + handleMetricsAutoCompleteError(state, err); + } } } diff --git a/public/app/plugins/datasource/graphite/state/providers.ts b/public/app/plugins/datasource/graphite/state/providers.ts index 6db0021d96a..4c59e641339 100644 --- a/public/app/plugins/datasource/graphite/state/providers.ts +++ b/public/app/plugins/datasource/graphite/state/providers.ts @@ -96,7 +96,9 @@ async function getAltSegments( return altSegments; } } catch (err) { - handleMetricsAutoCompleteError(state, err); + if (err instanceof Error) { + handleMetricsAutoCompleteError(state, err); + } } return []; @@ -133,7 +135,9 @@ async function getTags(state: GraphiteQueryEditorState, index: number, tagPrefix altTags.splice(0, 0, state.removeTagValue); return altTags; } catch (err) { - handleTagsAutoCompleteError(state, err); + if (err instanceof Error) { + handleTagsAutoCompleteError(state, err); + } } return []; @@ -168,7 +172,9 @@ async function getTagsAsSegments(state: GraphiteQueryEditorState, tagPrefix: str }); } catch (err) { tagsAsSegments = []; - handleTagsAutoCompleteError(state, err); + if (err instanceof Error) { + handleTagsAutoCompleteError(state, err); + } } return tagsAsSegments; diff --git a/public/app/plugins/datasource/graphite/types.ts b/public/app/plugins/datasource/graphite/types.ts index ce5d5436d85..23ede708119 100644 --- a/public/app/plugins/datasource/graphite/types.ts +++ b/public/app/plugins/datasource/graphite/types.ts @@ -41,6 +41,11 @@ export interface MetricTankMeta { info: MetricTankSeriesMeta[]; } +export interface GraphiteParserError { + message: string; + pos: number; +} + export type GraphiteQueryImportConfiguration = { loki: GraphiteToLokiQueryImportConfiguration; }; diff --git a/public/app/plugins/datasource/graphite/utils.ts b/public/app/plugins/datasource/graphite/utils.ts index 0a47176ccd0..88f0a5332cd 100644 --- a/public/app/plugins/datasource/graphite/utils.ts +++ b/public/app/plugins/datasource/graphite/utils.ts @@ -1,5 +1,7 @@ import { last } from 'lodash'; +import { GraphiteParserError } from './types'; + /** * Graphite-web before v1.6 returns HTTP 500 with full stack traces in an HTML page * when a query fails. It results in massive error alerts with HTML tags in the UI. @@ -19,3 +21,7 @@ export function reduceError(error: any): any { } return error; } + +export function isGraphiteParserError(e: unknown): e is GraphiteParserError { + return typeof e === 'object' && e !== null && 'message' in e && 'pos' in e; +} diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts index 604e9c53a36..4c0f58ca288 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts @@ -116,7 +116,9 @@ describe('InfluxDataSource', () => { try { await lastValueFrom(ctx.ds.query(queryOptions)); } catch (err) { - expect(err.message).toBe('InfluxDB Error: Query timeout'); + if (err instanceof Error) { + expect(err.message).toBe('InfluxDB Error: Query timeout'); + } } }); }); diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index 545750521ea..66a450d30bf 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -54,7 +54,9 @@ export function SearchForm({ datasource, query, onChange }: Props) { const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); return filteredOptions; } catch (error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + if (error instanceof Error) { + dispatch(notifyApp(createErrorNotification('Error', error))); + } return []; } finally { setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false })); diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.ts index f0fff40f33e..469dcb4ccb7 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.ts @@ -49,9 +49,11 @@ export function buildVisualQueryFromString(expr: string): Context { } catch (err) { // Not ideal to log it here, but otherwise we would lose the stack trace. console.error(err); - context.errors.push({ - text: err.message, - }); + if (err instanceof Error) { + context.errors.push({ + text: err.message, + }); + } } // If we have empty query, we want to reset errors diff --git a/public/app/plugins/datasource/opentsdb/query_ctrl.ts b/public/app/plugins/datasource/opentsdb/query_ctrl.ts index dc6bec377a3..7e71745f7c9 100644 --- a/public/app/plugins/datasource/opentsdb/query_ctrl.ts +++ b/public/app/plugins/datasource/opentsdb/query_ctrl.ts @@ -210,7 +210,9 @@ export class OpenTsQueryCtrl extends QueryCtrl { errs.downsampleInterval = "You must supply a downsample interval (e.g. '1m' or '1h')."; } } catch (err) { - errs.downsampleInterval = err.message; + if (err instanceof Error) { + errs.downsampleInterval = err.message; + } } } diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 113e53386e9..07e44b67135 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -13,7 +13,11 @@ import { Icon, } from '@grafana/ui'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; -import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; +import { + CancelablePromise, + isCancelablePromiseRejection, + makePromiseCancelable, +} from 'app/core/utils/CancelablePromise'; import { PrometheusDatasource } from '../datasource'; import { roundMsToMin } from '../language_utils'; @@ -174,7 +178,9 @@ class PromQueryField extends React.PureComponent>>(); const [spanOptions, setSpanOptions] = useState>>(); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [inputErrors, setInputErrors] = useState<{ [key: string]: boolean }>({}); const [isLoading, setIsLoading] = useState<{ serviceName: boolean; @@ -73,9 +73,9 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); return filteredOptions; } catch (error) { - if (error?.status === 404) { + if (isFetchError(error) && error?.status === 404) { setError(error); - } else { + } else if (error instanceof Error) { dispatch(notifyApp(createErrorNotification('Error', error))); } return []; @@ -94,9 +94,9 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props setSpanOptions(spans); } catch (error) { // Display message if Tempo is connected but search 404's - if (error?.status === 404) { + if (isFetchError(error) && error?.status === 404) { setError(error); - } else { + } else if (error instanceof Error) { dispatch(notifyApp(createErrorNotification('Error', error))); } } @@ -110,7 +110,9 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props await languageProvider.start(); setHasSyntaxLoaded(true); } catch (error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + if (error instanceof Error) { + dispatch(notifyApp(createErrorNotification('Error', error))); + } } }; fetchTags(); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index e00edda69f2..6ec7a4a40bd 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -190,7 +190,7 @@ export class TempoDatasource extends DataSourceWithBackend { }); return of({ data, state: LoadingState.Done }).pipe(delay(100)); } catch (ex) { - return of({ data: [], error: ex }).pipe(delay(100)); + return of({ + data: [], + error: ex instanceof Error ? ex : new Error('Unkown error'), + }).pipe(delay(100)); } } diff --git a/public/app/plugins/datasource/zipkin/QueryField.tsx b/public/app/plugins/datasource/zipkin/QueryField.tsx index ed63da354e1..68d16163fbb 100644 --- a/public/app/plugins/datasource/zipkin/QueryField.tsx +++ b/public/app/plugins/datasource/zipkin/QueryField.tsx @@ -131,7 +131,8 @@ export function useServices(datasource: ZipkinDatasource): AsyncState> = (pr const json = JSON.parse(data); return ; } catch (error) { - return `Invalid JSON provided: ${error.message}`; + if (error instanceof Error) { + return `Invalid JSON provided: ${error.message}`; + } else { + return 'Invalid JSON provided'; + } } }; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 13467292b78..71f95318fa3 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -571,7 +571,7 @@ class GraphElement { } } catch (e) { console.error('flotcharts error', e); - this.ctrl.error = e.message || 'Render Error'; + this.ctrl.error = e instanceof Error ? e.message : 'Render Error'; this.ctrl.renderError = true; } diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 95183a6a7f8..d979e204d1f 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -61,7 +61,7 @@ function sortSeriesByLabel(s1: { label: string }, s2: { label: string }) { label1 = parseHistogramLabel(s1.label); label2 = parseHistogramLabel(s2.label); } catch (err) { - console.error(err.message || err); + console.error(err instanceof Error ? err.message : err); return 0; } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 906224b24e2..e92ffdf00e4 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -449,7 +449,7 @@ export class HeatmapRenderer { return formattedValueToString(v); } } catch (err) { - console.error(err.message || err); + console.error(err instanceof Error ? err.message : err); } return value; }; diff --git a/public/app/plugins/panel/xychart/scatter.ts b/public/app/plugins/panel/xychart/scatter.ts index 022e9db6707..646d7b42f32 100644 --- a/public/app/plugins/panel/xychart/scatter.ts +++ b/public/app/plugins/panel/xychart/scatter.ts @@ -51,8 +51,9 @@ export function prepScatter( builder = prepConfig(getData, series, theme, ttip); } catch (e) { console.log('prepScatter ERROR', e); + const errorMessage = e instanceof Error ? e.message : 'Unknown error in prepScatter'; return { - error: e.message, + error: errorMessage, series: [], }; } diff --git a/public/test/matchers/toEmitValues.ts b/public/test/matchers/toEmitValues.ts index e8bf9e80470..80e96d7e32c 100644 --- a/public/test/matchers/toEmitValues.ts +++ b/public/test/matchers/toEmitValues.ts @@ -50,9 +50,10 @@ function tryExpectations(received: any[], expected: any[]): jest.CustomMatcherRe message: () => passMessage(received, expected), }; } catch (err) { + const message = err instanceof Error ? err.message : 'An unknown error occurred'; return { pass: false, - message: () => err, + message: () => message, }; } } diff --git a/tsconfig.json b/tsconfig.json index db195a464c3..5849b509339 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "allowJs": true, "strict": true, "resolveJsonModule": true, - "useUnknownInCatchVariables": false, + "useUnknownInCatchVariables": true, "incremental": true, "tsBuildInfoFile": "./tsconfig.tsbuildinfo" },