Chore: Enable useUnknownInCatchVariables for stricter type checking in catch blocks (#50591)

* wrap a bunch of errors

* wrap more things!

* fix up some unit tests

* wrap more errors

* tiny bit of tidy up
This commit is contained in:
Ashley Harrison 2022-06-15 08:59:29 +01:00 committed by GitHub
parent 2fbe99c1be
commit 803473f479
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 364 additions and 177 deletions

View File

@ -12,6 +12,7 @@ export { featureEnabled } from './utils/licensing';
export { logInfo, logDebug, logWarning, logError } from './utils/logging'; export { logInfo, logDebug, logWarning, logError } from './utils/logging';
export { export {
DataSourceWithBackend, DataSourceWithBackend,
HealthCheckError,
HealthCheckResult, HealthCheckResult,
HealthCheckResultDetails, HealthCheckResultDetails,
HealthStatus, HealthStatus,

View File

@ -123,6 +123,10 @@ export interface FetchError<T = any> {
config: BackendSrvRequest; 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, * 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} * a datasource etc. The BackendSrv is using the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}

View File

@ -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 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; details: HealthCheckResultDetails;
constructor(message: string, details: HealthCheckResultDetails) { constructor(message: string, details: HealthCheckResultDetails) {

View File

@ -1,4 +1,4 @@
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv, isFetchError } from '@grafana/runtime';
import { Role } from 'app/types'; import { Role } from 'app/types';
export const fetchRoleOptions = async (orgId?: number, query?: string): Promise<Role[]> => { export const fetchRoleOptions = async (orgId?: number, query?: string): Promise<Role[]> => {
@ -33,7 +33,9 @@ export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Ro
} }
return roles; return roles;
} catch (error) { } catch (error) {
error.isHandled = true; if (isFetchError(error)) {
error.isHandled = true;
}
return []; return [];
} }
}; };
@ -62,7 +64,9 @@ export const fetchTeamRoles = async (teamId: number, orgId?: number): Promise<Ro
} }
return roles; return roles;
} catch (error) { } catch (error) {
error.isHandled = true; if (isFetchError(error)) {
error.isHandled = true;
}
return []; return [];
} }
}; };

View File

@ -78,7 +78,7 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
try { try {
store.setObject(RICH_HISTORY_KEY, updatedHistory); store.setObject(RICH_HISTORY_KEY, updatedHistory);
} catch (error) { } catch (error) {
if (error.name === 'QuotaExceededError') { if (error instanceof Error && error.name === 'QuotaExceededError') {
throwError(RichHistoryServiceError.StorageFull, `Saving rich history failed: ${error.message}`); throwError(RichHistoryServiceError.StorageFull, `Saving rich history failed: ${error.message}`);
} else { } else {
throw error; throw error;

View File

@ -44,7 +44,9 @@ export class Store {
} catch (error) { } catch (error) {
// Likely hitting storage quota // Likely hitting storage quota
const errorToThrow = new Error(`Could not save item in localStorage: ${key}. [${error}]`); const errorToThrow = new Error(`Could not save item in localStorage: ${key}. [${error}]`);
errorToThrow.name = error.name; if (error instanceof Error) {
errorToThrow.name = error.name;
}
throw errorToThrow; throw errorToThrow;
} }
return true; return true;

View File

@ -5,12 +5,21 @@ export interface CancelablePromise<T> {
cancel: () => void; 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 = <T>(promise: Promise<T>): CancelablePromise<T> => { export const makePromiseCancelable = <T>(promise: Promise<T>): CancelablePromise<T> => {
let hasCanceled_ = false; let hasCanceled_ = false;
const wrappedPromise = new Promise<T>((resolve, reject) => { const wrappedPromise = new Promise<T>((resolve, reject) => {
promise.then((val) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val))); const canceledPromiseRejection: CancelablePromiseRejection = { isCanceled: true };
promise.catch((error) => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))); promise.then((val) => (hasCanceled_ ? reject(canceledPromiseRejection) : resolve(val)));
promise.catch((error) => (hasCanceled_ ? reject(canceledPromiseRejection) : reject(error)));
}); });
return { return {

View File

@ -58,11 +58,13 @@ export async function addToRichHistory(
}); });
warning = result.warning; warning = result.warning;
} catch (error) { } catch (error) {
if (error.name === RichHistoryServiceError.StorageFull) { if (error instanceof Error) {
richHistoryStorageFull = true; if (error.name === RichHistoryServiceError.StorageFull) {
showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message))); richHistoryStorageFull = true;
} else if (error.name !== RichHistoryServiceError.DuplicatedEntry) { showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message)));
dispatch(notifyApp(createErrorNotification('Rich History update failed', error.message))); } else if (error.name !== RichHistoryServiceError.DuplicatedEntry) {
dispatch(notifyApp(createErrorNotification('Rich History update failed', error.message)));
}
} }
// Saving failed. Do not add new entry. // Saving failed. Do not add new entry.
return { richHistoryStorageFull, limitExceeded }; return { richHistoryStorageFull, limitExceeded };
@ -101,7 +103,9 @@ export async function updateStarredInRichHistory(id: string, starred: boolean) {
try { try {
return await getRichHistoryStorage().updateStarred(id, starred); return await getRichHistoryStorage().updateStarred(id, starred);
} catch (error) { } 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; return undefined;
} }
} }
@ -110,7 +114,9 @@ export async function updateCommentInRichHistory(id: string, newComment: string
try { try {
return await getRichHistoryStorage().updateComment(id, newComment); return await getRichHistoryStorage().updateComment(id, newComment);
} catch (error) { } 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; return undefined;
} }
} }
@ -120,7 +126,9 @@ export async function deleteQueryInRichHistory(id: string) {
await getRichHistoryStorage().deleteRichHistory(id); await getRichHistoryStorage().deleteRichHistory(id);
return id; return id;
} catch (error) { } 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; return undefined;
} }
} }
@ -156,8 +164,9 @@ export async function migrateQueryHistoryFromLocalStorage(): Promise<LocalStorag
dispatch(notifyApp(createSuccessNotification('Query history successfully migrated from local storage'))); dispatch(notifyApp(createSuccessNotification('Query history successfully migrated from local storage')));
return { status: LocalStorageMigrationStatus.Successful }; return { status: LocalStorageMigrationStatus.Successful };
} catch (error) { } catch (error) {
dispatch(notifyApp(createWarningNotification(`Query history migration failed. ${error.message}`))); const errorToThrow = error instanceof Error ? error : new Error('Uknown error occurred.');
return { status: LocalStorageMigrationStatus.Failed, error }; dispatch(notifyApp(createWarningNotification(`Query history migration failed. ${errorToThrow.message}`)));
return { status: LocalStorageMigrationStatus.Failed, error: errorToThrow };
} }
} }

View File

@ -1,7 +1,7 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { dateTimeFormatTimeAgo } from '@grafana/data'; import { dateTimeFormatTimeAgo } from '@grafana/data';
import { featureEnabled, getBackendSrv, locationService } from '@grafana/runtime'; import { featureEnabled, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import config from 'app/core/config'; import config from 'app/core/config';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { accessControlQueryParam } from 'app/core/utils/accessControl';
@ -43,12 +43,14 @@ export function loadAdminUserPage(userId: number): ThunkResult<void> {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const userError = { if (isFetchError(error)) {
title: error.data.message, const userError = {
body: error.data.error, title: error.data.message,
}; body: error.data.error,
};
dispatch(userAdminPageFailedAction(userError)); dispatch(userAdminPageFailedAction(userError));
}
} }
}; };
} }
@ -212,12 +214,14 @@ export function loadLdapState(): ThunkResult<void> {
const connectionInfo = await getBackendSrv().get(`/api/admin/ldap/status`); const connectionInfo = await getBackendSrv().get(`/api/admin/ldap/status`);
dispatch(ldapConnectionInfoLoadedAction(connectionInfo)); dispatch(ldapConnectionInfoLoadedAction(connectionInfo));
} catch (error) { } catch (error) {
error.isHandled = true; if (isFetchError(error)) {
const ldapError = { error.isHandled = true;
title: error.data.message, const ldapError = {
body: error.data.error, title: error.data.message,
}; body: error.data.error,
dispatch(ldapFailedAction(ldapError)); };
dispatch(ldapFailedAction(ldapError));
}
} }
}; };
} }
@ -235,13 +239,15 @@ export function loadUserMapping(username: string): ThunkResult<void> {
}; };
dispatch(userMappingInfoLoadedAction(userInfo)); dispatch(userMappingInfoLoadedAction(userInfo));
} catch (error) { } catch (error) {
error.isHandled = true; if (isFetchError(error)) {
const userError = { error.isHandled = true;
title: error.data.message, const userError = {
body: error.data.error, title: error.data.message,
}; body: error.data.error,
dispatch(clearUserMappingInfoAction()); };
dispatch(userMappingInfoFailedAction(userError)); dispatch(clearUserMappingInfoAction());
dispatch(userMappingInfoFailedAction(userError));
}
} }
}; };
} }

View File

@ -1,4 +1,4 @@
import { getBackendSrv, locationService } from '@grafana/runtime'; import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types'; import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
@ -28,7 +28,9 @@ export function createNotificationChannel(data: any): ThunkResult<Promise<void>>
dispatch(notifyApp(createSuccessNotification('Notification created'))); dispatch(notifyApp(createSuccessNotification('Notification created')));
locationService.push('/alerting/notifications'); locationService.push('/alerting/notifications');
} catch (error) { } 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<void> {
await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data); await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data);
dispatch(notifyApp(createSuccessNotification('Notification updated'))); dispatch(notifyApp(createSuccessNotification('Notification updated')));
} catch (error) { } catch (error) {
dispatch(notifyApp(createErrorNotification(error.data.error))); if (isFetchError(error)) {
dispatch(notifyApp(createErrorNotification(error.data.error)));
}
} }
}; };
} }

View File

@ -1,7 +1,7 @@
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { urlUtil } from '@grafana/data'; import { urlUtil } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv, isFetchError } from '@grafana/runtime';
import { import {
AlertmanagerAlert, AlertmanagerAlert,
AlertManagerCortexConfig, AlertManagerCortexConfig,
@ -18,7 +18,6 @@ import {
ExternalAlertmanagerConfig, ExternalAlertmanagerConfig,
} from 'app/plugins/datasource/alertmanager/types'; } from 'app/plugins/datasource/alertmanager/types';
import { isFetchError } from '../utils/alertmanager';
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
// "grafana" for grafana-managed, otherwise a datasource name // "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 no config has been uploaded to grafana, it returns error instead of latest config
if ( if (
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
isFetchError(e) &&
e.data?.message?.includes('could not find an Alertmanager configuration') e.data?.message?.includes('could not find an Alertmanager configuration')
) { ) {
return { return {

View File

@ -12,6 +12,7 @@ jest.mock('./prometheus');
jest.mock('./ruler'); jest.mock('./ruler');
jest.mock('app/core/services/context_srv', () => {}); jest.mock('app/core/services/context_srv', () => {});
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch }), getBackendSrv: () => ({ fetch }),
})); }));

View File

@ -1,9 +1,8 @@
import { lastValueFrom } from 'rxjs'; 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 { PromApplication, PromApiFeatures, PromBuildInfoResponse } from 'app/types/unified-alerting-dto';
import { isFetchError } from '../utils/alertmanager';
import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
import { getDataSourceByName } from '../utils/datasource'; import { getDataSourceByName } from '../utils/datasource';

View File

@ -112,7 +112,7 @@ export default function AlertmanagerConfig(): JSX.Element {
JSON.parse(v); JSON.parse(v);
return true; return true;
} catch (e) { } catch (e) {
return e.message; return e instanceof Error ? e.message : 'Invalid JSON.';
} }
}, },
})} })}

View File

@ -1,5 +1,4 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { FetchError } from '@grafana/runtime';
import { import {
AlertManagerCortexConfig, AlertManagerCortexConfig,
MatcherOperator, MatcherOperator,
@ -260,7 +259,3 @@ export function getMonthsString(months?: string[]): string {
export function getYearsString(years?: string[]): string { export function getYearsString(years?: string[]): string {
return 'Years: ' + (years?.join(', ') ?? 'All'); 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;
}

View File

@ -1,11 +1,9 @@
import { AsyncThunk, createSlice, Draft, isAsyncThunkAction, PayloadAction, SerializedError } from '@reduxjs/toolkit'; import { AsyncThunk, createSlice, Draft, isAsyncThunkAction, PayloadAction, SerializedError } from '@reduxjs/toolkit';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { FetchError } from '@grafana/runtime'; import { FetchError, isFetchError } from '@grafana/runtime';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { isFetchError } from './alertmanager';
export interface AsyncRequestState<T> { export interface AsyncRequestState<T> {
result?: T; result?: T;
loading: boolean; loading: boolean;

View File

@ -82,7 +82,7 @@ export const validateIntervals = (
getValidIntervals(intervals, dependencies); getValidIntervals(intervals, dependencies);
return null; return null;
} catch (err) { } catch (err) {
return err.message; return err instanceof Error ? err.message : 'Invalid intervals';
} }
}; };

View File

@ -6,7 +6,7 @@ import { Subscription } from 'rxjs';
import { FieldConfigSource, GrafanaTheme2 } from '@grafana/data'; import { FieldConfigSource, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime'; import { isFetchError, locationService } from '@grafana/runtime';
import { import {
HorizontalGroup, HorizontalGroup,
InlineSwitch, InlineSwitch,
@ -163,7 +163,9 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
await saveAndRefreshLibraryPanel(this.props.panel, this.props.dashboard.meta.folderId!); await saveAndRefreshLibraryPanel(this.props.panel, this.props.dashboard.meta.folderId!);
this.props.notifyApp(createPanelLibrarySuccessNotification('Library panel saved')); this.props.notifyApp(createPanelLibrarySuccessNotification('Library panel saved'));
} catch (err) { } 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; return;
} }

View File

@ -64,7 +64,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
await validationSrv.validateNewDashboardName(getFormValues().$folder.id, dashboardName); await validationSrv.validateNewDashboardName(getFormValues().$folder.id, dashboardName);
return true; return true;
} catch (e) { } catch (e) {
return e.message; return e instanceof Error ? e.message : 'Dashboard name is invalid';
} }
}; };

View File

@ -2,7 +2,7 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { Subject } from 'rxjs'; 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 { getBackendSrv } from 'app/core/services/backend_srv';
import { keybindingSrv } from 'app/core/services/keybindingSrv'; import { keybindingSrv } from 'app/core/services/keybindingSrv';
import { variableAdapters } from 'app/features/variables/adapters'; import { variableAdapters } from 'app/features/variables/adapters';
@ -208,7 +208,15 @@ describeInitScenario('Initializing home dashboard', (ctx) => {
describeInitScenario('Initializing home dashboard cancelled', (ctx) => { describeInitScenario('Initializing home dashboard cancelled', (ctx) => {
ctx.setup(() => { ctx.setup(() => {
ctx.args.routeName = DashboardRoutes.Home; 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', () => { it('Should abort init process', () => {

View File

@ -1,5 +1,5 @@
import { locationUtil, setWeekStart } from '@grafana/data'; 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 { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
@ -90,7 +90,7 @@ async function fetchDashboard(
} }
} catch (err) { } catch (err) {
// Ignore cancelled errors // Ignore cancelled errors
if (err.cancelled) { if (isFetchError(err) && err.cancelled) {
return null; return null;
} }
@ -184,7 +184,9 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
keybindingSrv.setupDashboardBindings(dashboard); keybindingSrv.setupDashboardBindings(dashboard);
} catch (err) { } catch (err) {
dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); if (err instanceof Error) {
dispatch(notifyApp(createErrorNotification('Dashboard init failed', err)));
}
console.error(err); console.error(err);
} }

View File

@ -1,7 +1,7 @@
import { of } from 'rxjs'; import { of } from 'rxjs';
import { thunkTester } from 'test/core/thunk/thunkTester'; 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 { getBackendSrv } from 'app/core/services/backend_srv';
import { ThunkResult, ThunkDispatch } from 'app/types'; import { ThunkResult, ThunkDispatch } from 'app/types';
@ -316,13 +316,17 @@ describe('testDataSource', () => {
it('then testDataSourceFailed should be dispatched with response error message', async () => { it('then testDataSourceFailed should be dispatched with response error message', async () => {
const result = { const result = {
message: 'Error testing datasource', message: 'Response error message',
}; };
const dispatchedActions = await failDataSourceTest({ const error: FetchError = {
message: 'Error testing datasource', config: {
url: '',
},
data: { message: 'Response error message' }, data: { message: 'Response error message' },
status: 400,
statusText: 'Bad Request', statusText: 'Bad Request',
}); };
const dispatchedActions = await failDataSourceTest(error);
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
}); });
@ -330,10 +334,15 @@ describe('testDataSource', () => {
const result = { const result = {
message: 'Response error message', message: 'Response error message',
}; };
const dispatchedActions = await failDataSourceTest({ const error: FetchError = {
config: {
url: '',
},
data: { message: 'Response error message' }, data: { message: 'Response error message' },
status: 400,
statusText: 'Bad Request', statusText: 'Bad Request',
}); };
const dispatchedActions = await failDataSourceTest(error);
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
}); });
@ -341,7 +350,15 @@ describe('testDataSource', () => {
const result = { const result = {
message: 'HTTP error Bad Request', 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)]); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
}); });
}); });

View File

@ -1,7 +1,14 @@
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { DataSourcePluginMeta, DataSourceSettings, locationUtil } from '@grafana/data'; 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 { updateNavIndex } from 'app/core/actions';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { accessControlQueryParam } from 'app/core/utils/accessControl';
@ -77,7 +84,9 @@ export const initDataSourceSettings = (
dispatch(initDataSourceSettingsSucceeded(importedPlugin)); dispatch(initDataSourceSettingsSucceeded(importedPlugin));
} catch (err) { } catch (err) {
dispatch(initDataSourceSettingsFailed(err)); if (err instanceof Error) {
dispatch(initDataSourceSettingsFailed(err));
}
} }
}; };
}; };
@ -104,9 +113,17 @@ export const testDataSource = (
dispatch(testDataSourceSucceeded(result)); dispatch(testDataSourceSucceeded(result));
} catch (err) { } 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 })); dispatch(testDataSourceFailed({ message, details }));
} }

View File

@ -1,7 +1,7 @@
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { locationUtil } from '@grafana/data'; 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 { notifyApp, updateNavIndex } from 'app/core/actions';
import { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification'; import { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
@ -59,7 +59,7 @@ export function checkFolderPermissions(uid: string): ThunkResult<void> {
); );
dispatch(setCanViewFolderPermissions(true)); dispatch(setCanViewFolderPermissions(true));
} catch (err) { } catch (err) {
if (err.status !== 403) { if (isFetchError(err) && err.status !== 403) {
dispatch(notifyApp(createWarningNotification('Error checking folder permissions', err.data?.message))); dispatch(notifyApp(createWarningNotification('Error checking folder permissions', err.data?.message)));
} }

View File

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useAsync, useDebounce } from 'react-use'; import { useAsync, useDebounce } from 'react-use';
import { isFetchError } from '@grafana/runtime';
import { Button, Field, Input, Modal } from '@grafana/ui'; import { Button, Field, Input, Modal } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { FolderPicker } from 'app/core/components/Select/FolderPicker';
@ -36,7 +37,9 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
try { try {
return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderId === folderId); return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderId === folderId);
} catch (err) { } catch (err) {
err.isHandled = true; if (isFetchError(err)) {
err.isHandled = true;
}
return true; return true;
} finally { } finally {
setWaiting(false); setWaiting(false);

View File

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import useAsyncFn from 'react-use/lib/useAsyncFn'; import useAsyncFn from 'react-use/lib/useAsyncFn';
import { isFetchError } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { PanelModel } from 'app/features/dashboard/state'; import { PanelModel } from 'app/features/dashboard/state';
@ -17,8 +18,11 @@ export const usePanelSave = () => {
try { try {
return await saveAndRefreshLibraryPanel(panel, folderId); return await saveAndRefreshLibraryPanel(panel, folderId);
} catch (err) { } catch (err) {
err.isHandled = true; if (isFetchError(err)) {
throw new Error(err.data.message); err.isHandled = true;
throw new Error(err.data.message);
}
throw err;
} }
}, []); }, []);

View File

@ -84,10 +84,12 @@ class UnthemedDashboardImport extends PureComponent<Props> {
try { try {
dashboard = JSON.parse(e.target.result); dashboard = JSON.parse(e.target.result);
} catch (error) { } catch (error) {
appEvents.emit(AppEvents.alertError, [ if (error instanceof Error) {
'Import failed', appEvents.emit(AppEvents.alertError, [
'JSON -> JS Serialization failed: ' + error.message, 'Import failed',
]); 'JSON -> JS Serialization failed: ' + error.message,
]);
}
return; return;
} }
importDashboardJson(dashboard); importDashboardJson(dashboard);

View File

@ -1,5 +1,5 @@
import { DataSourceInstanceSettings, locationUtil } from '@grafana/data'; 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 { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
@ -34,7 +34,9 @@ export function fetchGcomDashboard(id: string): ThunkResult<void> {
dispatch(processElements(dashboard.json)); dispatch(processElements(dashboard.json));
} catch (error) { } catch (error) {
dispatch(fetchFailed()); 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); await saveDashboard(options);
return { succeeded: true }; return { succeeded: true };
} catch (err) { } catch (err) {
if (err.data?.status !== 'plugin-dashboard') { if (isFetchError(err)) {
return { succeeded: false }; if (err.data?.status !== 'plugin-dashboard') {
} return { succeeded: false };
}
err.isHandled = true; err.isHandled = true;
}
options.overwrite = true; options.overwrite = true;
try { try {

View File

@ -33,6 +33,8 @@ async function withErrorHandling(apiCall: () => Promise<void>, message = 'Playli
await apiCall(); await apiCall();
dispatch(notifyApp(createSuccessNotification(message))); dispatch(notifyApp(createSuccessNotification(message)));
} catch (e) { } catch (e) {
dispatch(notifyApp(createErrorNotification('Unable to save playlist', e))); if (e instanceof Error) {
dispatch(notifyApp(createErrorNotification('Unable to save playlist', e)));
}
} }
} }

View File

@ -1,5 +1,5 @@
import { PluginError, PluginMeta, renderMarkdown } from '@grafana/data'; 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 { API_ROOT, GCOM_API_ROOT } from './constants';
import { isLocalPluginVisible, isRemotePluginVisible } from './helpers'; import { isLocalPluginVisible, isRemotePluginVisible } from './helpers';
@ -45,8 +45,10 @@ async function getRemotePlugin(id: string): Promise<RemotePlugin | undefined> {
try { try {
return await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}`, {}); return await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}`, {});
} catch (error) { } catch (error) {
// It can happen that GCOM is not available, in that case we show a limited set of information to the user. if (isFetchError(error)) {
error.isHandled = true; // 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; return;
} }
} }
@ -66,8 +68,10 @@ async function getPluginVersions(id: string, isPublished: boolean): Promise<Vers
grafanaDependency: v.grafanaDependency, grafanaDependency: v.grafanaDependency,
})); }));
} catch (error) { } catch (error) {
// It can happen that GCOM is not available, in that case we show a limited set of information to the user. if (isFetchError(error)) {
error.isHandled = true; // 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 []; return [];
} }
} }
@ -79,7 +83,9 @@ async function getLocalPluginReadme(id: string): Promise<string> {
return markdownAsHtml; return markdownAsHtml;
} catch (error) { } catch (error) {
error.isHandled = true; if (isFetchError(error)) {
error.isHandled = true;
}
return ''; return '';
} }
} }

View File

@ -1,7 +1,7 @@
import { createAction, createAsyncThunk, Update } from '@reduxjs/toolkit'; import { createAction, createAsyncThunk, Update } from '@reduxjs/toolkit';
import { PanelPlugin } from '@grafana/data'; import { PanelPlugin } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv, isFetchError } from '@grafana/runtime';
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
import { StoreState, ThunkResult } from 'app/types'; import { StoreState, ThunkResult } from 'app/types';
@ -39,7 +39,9 @@ export const fetchRemotePlugins = createAsyncThunk<RemotePlugin[], void, { rejec
try { try {
return await getRemotePlugins(); return await getRemotePlugins();
} catch (error) { } catch (error) {
error.isHandled = true; if (isFetchError(error)) {
error.isHandled = true;
}
return thunkApi.rejectWithValue([]); return thunkApi.rejectWithValue([]);
} }
} }

View File

@ -191,7 +191,9 @@ export class DatasourceSrv implements DataSourceService {
this.datasources[instance.uid] = instance; this.datasources[instance.uid] = instance;
return instance; return instance;
} catch (err) { } catch (err) {
appEvents.emit(AppEvents.alertError, [instanceSettings.name + ' plugin failed', err.toString()]); if (err instanceof Error) {
appEvents.emit(AppEvents.alertError, [instanceSettings.name + ' plugin failed', err.toString()]);
}
return Promise.reject({ message: `Datasource: ${key} was not found` }); return Promise.reject({ message: `Datasource: ${key} was not found` });
} }
} }

View File

@ -211,7 +211,7 @@ describe('query actions', () => {
silenceConsoleOutput(); silenceConsoleOutput();
it('then correct actions are dispatched', async () => { it('then correct actions are dispatched', async () => {
const variable = createVariable({ includeAll: true }); 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)); 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 })) toKeyedAction('key', addVariableEditorError({ errorProp: 'update', errorText: error.message }))
); );
expect(dispatchedActions[3]).toEqual( expect(dispatchedActions[3]).toEqual(
toKeyedAction( toKeyedAction('key', variableStateFailed(toVariablePayload(variable, { error })))
'key',
variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } }))
)
); );
expect(dispatchedActions[4].type).toEqual(notifyApp.type); expect(dispatchedActions[4].type).toEqual(notifyApp.type);
expect(dispatchedActions[4].payload.title).toEqual('Templating [0]'); expect(dispatchedActions[4].payload.title).toEqual('Templating [0]');

View File

@ -47,15 +47,17 @@ export const updateQueryVariableOptions = (
getVariableQueryRunner().queueRequest({ identifier, datasource, searchFilter }); getVariableQueryRunner().queueRequest({ identifier, datasource, searchFilter });
}); });
} catch (err) { } catch (err) {
const error = toDataQueryError(err); if (err instanceof Error) {
const { rootStateKey } = identifier; const error = toDataQueryError(err);
if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) { const { rootStateKey } = identifier;
dispatch( if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) {
toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message })) dispatch(
); toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message }))
} );
}
throw error; throw error;
}
} }
}; };
}; };

View File

@ -116,7 +116,9 @@ function useTimoutValidation(value: string | undefined) {
rangeUtil.describeInterval(value); rangeUtil.describeInterval(value);
setErr(undefined); setErr(undefined);
} catch (e) { } catch (e) {
setErr(e.toString()); if (e instanceof Error) {
setErr(e.toString());
}
} }
} else { } else {
setErr(undefined); setErr(undefined);

View File

@ -1,7 +1,7 @@
import { filter, find, startsWith } from 'lodash'; import { filter, find, startsWith } from 'lodash';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; 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 { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { resourceTypeDisplayNames } from '../azureMetadata'; import { resourceTypeDisplayNames } from '../azureMetadata';
@ -314,14 +314,18 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
}); });
} catch (e) { } catch (e) {
let message = 'Azure Monitor: '; let message = 'Azure Monitor: ';
message += e.statusText ? e.statusText + ': ' : ''; if (isFetchError(e)) {
message += e.statusText ? e.statusText + ': ' : '';
if (e.data && e.data.error && e.data.error.code) { if (e.data && e.data.error && e.data.error.code) {
message += e.data.error.code + '. ' + e.data.error.message; message += e.data.error.code + '. ' + e.data.error.message;
} else if (e.data && e.data.error) { } else if (e.data && e.data.error) {
message += e.data.error; message += e.data.error;
} else if (e.data) { } else if (e.data) {
message += e.data; message += e.data;
} else {
message += 'Cannot connect to Azure Monitor REST API.';
}
} else { } else {
message += 'Cannot connect to Azure Monitor REST API.'; message += 'Cannot connect to Azure Monitor REST API.';
} }

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data'; import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data';
import { getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime';
import { Alert } from '@grafana/ui'; import { Alert } from '@grafana/ui';
import ResponseParser from '../azure_monitor/response_parser'; import ResponseParser from '../azure_monitor/response_parser';
@ -70,13 +70,15 @@ export class ConfigEditor extends PureComponent<Props, State> {
this.setState({ error: undefined }); this.setState({ error: undefined });
return ResponseParser.parseSubscriptionsForSelect(result); return ResponseParser.parseSubscriptionsForSelect(result);
} catch (err) { } catch (err) {
this.setState({ if (isFetchError(err)) {
error: { this.setState({
title: 'Error requesting subscriptions', error: {
description: 'Could not request subscriptions from Azure. Check your credentials and try again.', title: 'Error requesting subscriptions',
details: err?.data?.message, description: 'Could not request subscriptions from Azure. Check your credentials and try again.',
}, details: err?.data?.message,
}); },
});
}
return Promise.resolve([]); return Promise.resolve([]);
} }
}; };

View File

@ -93,7 +93,11 @@ describe('AzureMonitor resourcePickerData', () => {
await resourcePickerData.getSubscriptions(); await resourcePickerData.getSubscriptions();
throw Error('expected getSubscriptions to fail but it succeeded'); throw Error('expected getSubscriptions to fail but it succeeded');
} catch (err) { } 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'); await resourcePickerData.getResourceGroupsBySubscriptionId('123');
throw Error('expected getResourceGroupsBySubscriptionId to fail but it succeeded'); throw Error('expected getResourceGroupsBySubscriptionId to fail but it succeeded');
} catch (err) { } 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'); await resourcePickerData.getResourcesForResourceGroup('dev');
throw Error('expected getResourcesForResourceGroup to fail but it succeeded'); throw Error('expected getResourcesForResourceGroup to fail but it succeeded');
} catch (err) { } 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'); await resourcePickerData.search('dev', 'logs');
throw Error('expected search test to fail but it succeeded'); throw Error('expected search test to fail but it succeeded');
} catch (err) { } 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;
}
} }
}); });
}); });

View File

@ -79,8 +79,10 @@ export default class GraphiteQuery {
try { try {
this.parseTargetRecursive(astNode, null); this.parseTargetRecursive(astNode, null);
} catch (err) { } catch (err) {
console.error('error parsing target:', err.message); if (err instanceof Error) {
this.error = err.message; console.error('error parsing target:', err.message);
this.error = err.message;
}
this.target.textEditor = true; this.target.textEditor = true;
} }

View File

@ -1,4 +1,6 @@
import { Lexer } from './lexer'; import { Lexer } from './lexer';
import { GraphiteParserError } from './types';
import { isGraphiteParserError } from './utils';
export class Parser { export class Parser {
expression: any; expression: any;
@ -21,11 +23,13 @@ export class Parser {
try { try {
return this.functionCall() || this.metricExpression(); return this.functionCall() || this.metricExpression();
} catch (e) { } catch (e) {
return { if (isGraphiteParserError(e)) {
type: 'error', return {
message: e.message, type: 'error',
pos: e.pos, message: e.message,
}; pos: e.pos,
};
}
} }
} }
@ -222,7 +226,11 @@ export class Parser {
const token = this.consumeToken(); const token = this.consumeToken();
if (token.isUnclosed) { if (token.isUnclosed) {
throw { message: 'Unclosed string parameter', pos: token.pos }; const error: GraphiteParserError = {
message: 'Unclosed string parameter',
pos: token.pos,
};
throw error;
} }
return { return {
@ -234,10 +242,11 @@ export class Parser {
errorMark(text: string) { errorMark(text: string) {
const currentToken = this.tokens[this.index]; const currentToken = this.tokens[this.index];
const type = currentToken ? currentToken.type : 'end of string'; const type = currentToken ? currentToken.type : 'end of string';
throw { const error: GraphiteParserError = {
message: text + ' instead found ' + type, message: text + ' instead found ' + type,
pos: currentToken ? currentToken.pos : this.lexer.char, pos: currentToken ? currentToken.pos : this.lexer.char,
}; };
throw error;
} }
// returns token value and incre // returns token value and incre

View File

@ -90,7 +90,9 @@ export async function checkOtherSegments(
} }
} }
} catch (err) { } catch (err) {
handleMetricsAutoCompleteError(state, err); if (err instanceof Error) {
handleMetricsAutoCompleteError(state, err);
}
} }
} }

View File

@ -96,7 +96,9 @@ async function getAltSegments(
return altSegments; return altSegments;
} }
} catch (err) { } catch (err) {
handleMetricsAutoCompleteError(state, err); if (err instanceof Error) {
handleMetricsAutoCompleteError(state, err);
}
} }
return []; return [];
@ -133,7 +135,9 @@ async function getTags(state: GraphiteQueryEditorState, index: number, tagPrefix
altTags.splice(0, 0, state.removeTagValue); altTags.splice(0, 0, state.removeTagValue);
return altTags; return altTags;
} catch (err) { } catch (err) {
handleTagsAutoCompleteError(state, err); if (err instanceof Error) {
handleTagsAutoCompleteError(state, err);
}
} }
return []; return [];
@ -168,7 +172,9 @@ async function getTagsAsSegments(state: GraphiteQueryEditorState, tagPrefix: str
}); });
} catch (err) { } catch (err) {
tagsAsSegments = []; tagsAsSegments = [];
handleTagsAutoCompleteError(state, err); if (err instanceof Error) {
handleTagsAutoCompleteError(state, err);
}
} }
return tagsAsSegments; return tagsAsSegments;

View File

@ -41,6 +41,11 @@ export interface MetricTankMeta {
info: MetricTankSeriesMeta[]; info: MetricTankSeriesMeta[];
} }
export interface GraphiteParserError {
message: string;
pos: number;
}
export type GraphiteQueryImportConfiguration = { export type GraphiteQueryImportConfiguration = {
loki: GraphiteToLokiQueryImportConfiguration; loki: GraphiteToLokiQueryImportConfiguration;
}; };

View File

@ -1,5 +1,7 @@
import { last } from 'lodash'; import { last } from 'lodash';
import { GraphiteParserError } from './types';
/** /**
* Graphite-web before v1.6 returns HTTP 500 with full stack traces in an HTML page * 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. * 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; return error;
} }
export function isGraphiteParserError(e: unknown): e is GraphiteParserError {
return typeof e === 'object' && e !== null && 'message' in e && 'pos' in e;
}

View File

@ -116,7 +116,9 @@ describe('InfluxDataSource', () => {
try { try {
await lastValueFrom(ctx.ds.query(queryOptions)); await lastValueFrom(ctx.ds.query(queryOptions));
} catch (err) { } catch (err) {
expect(err.message).toBe('InfluxDB Error: Query timeout'); if (err instanceof Error) {
expect(err.message).toBe('InfluxDB Error: Query timeout');
}
} }
}); });
}); });

View File

@ -54,7 +54,9 @@ export function SearchForm({ datasource, query, onChange }: Props) {
const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false));
return filteredOptions; return filteredOptions;
} catch (error) { } catch (error) {
dispatch(notifyApp(createErrorNotification('Error', error))); if (error instanceof Error) {
dispatch(notifyApp(createErrorNotification('Error', error)));
}
return []; return [];
} finally { } finally {
setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false })); setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false }));

View File

@ -49,9 +49,11 @@ export function buildVisualQueryFromString(expr: string): Context {
} catch (err) { } catch (err) {
// Not ideal to log it here, but otherwise we would lose the stack trace. // Not ideal to log it here, but otherwise we would lose the stack trace.
console.error(err); console.error(err);
context.errors.push({ if (err instanceof Error) {
text: err.message, context.errors.push({
}); text: err.message,
});
}
} }
// If we have empty query, we want to reset errors // If we have empty query, we want to reset errors

View File

@ -210,7 +210,9 @@ export class OpenTsQueryCtrl extends QueryCtrl {
errs.downsampleInterval = "You must supply a downsample interval (e.g. '1m' or '1h')."; errs.downsampleInterval = "You must supply a downsample interval (e.g. '1m' or '1h').";
} }
} catch (err) { } catch (err) {
errs.downsampleInterval = err.message; if (err instanceof Error) {
errs.downsampleInterval = err.message;
}
} }
} }

View File

@ -13,7 +13,11 @@ import {
Icon, Icon,
} from '@grafana/ui'; } from '@grafana/ui';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; 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 { PrometheusDatasource } from '../datasource';
import { roundMsToMin } from '../language_utils'; import { roundMsToMin } from '../language_utils';
@ -174,7 +178,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
await Promise.all(remainingTasks); await Promise.all(remainingTasks);
this.onUpdateLanguage(); this.onUpdateLanguage();
} catch (err) { } catch (err) {
if (!err.isCanceled) { if (isCancelablePromiseRejection(err) && err.isCanceled) {
// do nothing, promise was canceled
} else {
throw err; throw err;
} }
} }

View File

@ -31,6 +31,7 @@ import {
DataSourceWithBackend, DataSourceWithBackend,
BackendDataSourceResponse, BackendDataSourceResponse,
toDataQueryResponse, toDataQueryResponse,
isFetchError,
} from '@grafana/runtime'; } from '@grafana/runtime';
import { Badge, BadgeColor, Tooltip } from '@grafana/ui'; import { Badge, BadgeColor, Tooltip } from '@grafana/ui';
import { safeStringifyValue } from 'app/core/utils/explore'; import { safeStringifyValue } from 'app/core/utils/explore';
@ -224,7 +225,7 @@ export class PrometheusDatasource
); );
} catch (err) { } catch (err) {
// If status code of error is Method Not Allowed (405) and HTTP method is POST, retry with GET // If status code of error is Method Not Allowed (405) and HTTP method is POST, retry with GET
if (this.httpMethod === 'POST' && (err.status === 405 || err.status === 400)) { if (this.httpMethod === 'POST' && isFetchError(err) && (err.status === 405 || err.status === 400)) {
console.warn(`Couldn't use configured POST HTTP method for this request. Trying to use GET method instead.`); console.warn(`Couldn't use configured POST HTTP method for this request. Trying to use GET method instead.`);
} else { } else {
throw err; throw err;

View File

@ -43,9 +43,11 @@ export function buildVisualQueryFromString(expr: string): Context {
} catch (err) { } catch (err) {
// Not ideal to log it here, but otherwise we would lose the stack trace. // Not ideal to log it here, but otherwise we would lose the stack trace.
console.error(err); console.error(err);
context.errors.push({ if (err instanceof Error) {
text: err.message, context.errors.push({
}); text: err.message,
});
}
} }
// If we have empty query, we want to reset errors // If we have empty query, we want to reset errors

View File

@ -4,7 +4,7 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { Node } from 'slate'; import { Node } from 'slate';
import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime';
import { import {
InlineFieldRow, InlineFieldRow,
InlineField, InlineField,
@ -53,7 +53,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
const [hasSyntaxLoaded, setHasSyntaxLoaded] = useState(false); const [hasSyntaxLoaded, setHasSyntaxLoaded] = useState(false);
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>(); const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
const [spanOptions, setSpanOptions] = useState<Array<SelectableValue<string>>>(); const [spanOptions, setSpanOptions] = useState<Array<SelectableValue<string>>>();
const [error, setError] = useState(null); const [error, setError] = useState<Error | FetchError | null>(null);
const [inputErrors, setInputErrors] = useState<{ [key: string]: boolean }>({}); const [inputErrors, setInputErrors] = useState<{ [key: string]: boolean }>({});
const [isLoading, setIsLoading] = useState<{ const [isLoading, setIsLoading] = useState<{
serviceName: boolean; 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)); const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false));
return filteredOptions; return filteredOptions;
} catch (error) { } catch (error) {
if (error?.status === 404) { if (isFetchError(error) && error?.status === 404) {
setError(error); setError(error);
} else { } else if (error instanceof Error) {
dispatch(notifyApp(createErrorNotification('Error', error))); dispatch(notifyApp(createErrorNotification('Error', error)));
} }
return []; return [];
@ -94,9 +94,9 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
setSpanOptions(spans); setSpanOptions(spans);
} catch (error) { } catch (error) {
// Display message if Tempo is connected but search 404's // Display message if Tempo is connected but search 404's
if (error?.status === 404) { if (isFetchError(error) && error?.status === 404) {
setError(error); setError(error);
} else { } else if (error instanceof Error) {
dispatch(notifyApp(createErrorNotification('Error', error))); dispatch(notifyApp(createErrorNotification('Error', error)));
} }
} }
@ -110,7 +110,9 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
await languageProvider.start(); await languageProvider.start();
setHasSyntaxLoaded(true); setHasSyntaxLoaded(true);
} catch (error) { } catch (error) {
dispatch(notifyApp(createErrorNotification('Error', error))); if (error instanceof Error) {
dispatch(notifyApp(createErrorNotification('Error', error)));
}
} }
}; };
fetchTags(); fetchTags();

View File

@ -190,7 +190,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
) )
); );
} catch (error) { } catch (error) {
return of({ error: { message: error.message }, data: [] }); return of({ error: { message: error instanceof Error ? error.message : 'Unknown error occurred' }, data: [] });
} }
} }

View File

@ -222,7 +222,10 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
}); });
return of({ data, state: LoadingState.Done }).pipe(delay(100)); return of({ data, state: LoadingState.Done }).pipe(delay(100));
} catch (ex) { } 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));
} }
} }

View File

@ -131,7 +131,8 @@ export function useServices(datasource: ZipkinDatasource): AsyncState<CascaderOp
} }
return []; return [];
} catch (error) { } catch (error) {
dispatch(notifyApp(createErrorNotification('Failed to load services from Zipkin', error))); const errorToShow = error instanceof Error ? error : 'An unknown error occurred';
dispatch(notifyApp(createErrorNotification('Failed to load services from Zipkin', errorToShow)));
throw error; throw error;
} }
}, [datasource]); }, [datasource]);
@ -175,7 +176,8 @@ export function useLoadOptions(datasource: ZipkinDatasource) {
}); });
} }
} catch (error) { } catch (error) {
dispatch(notifyApp(createErrorNotification('Failed to load spans from Zipkin', error))); const errorToShow = error instanceof Error ? error : 'An unknown error occurred';
dispatch(notifyApp(createErrorNotification('Failed to load spans from Zipkin', errorToShow)));
throw error; throw error;
} }
}, },
@ -216,7 +218,8 @@ export function useLoadOptions(datasource: ZipkinDatasource) {
}); });
} }
} catch (error) { } catch (error) {
dispatch(notifyApp(createErrorNotification('Failed to load spans from Zipkin', error))); const errorToShow = error instanceof Error ? error : 'An unknown error occurred';
dispatch(notifyApp(createErrorNotification('Failed to load spans from Zipkin', errorToShow)));
throw error; throw error;
} }
}, },

View File

@ -68,7 +68,11 @@ export const APIEditor: FC<StandardEditorProps<APIEditorConfig, any, any>> = (pr
const json = JSON.parse(data); const json = JSON.parse(data);
return <JSONFormatter json={json} />; return <JSONFormatter json={json} />;
} catch (error) { } catch (error) {
return `Invalid JSON provided: ${error.message}`; if (error instanceof Error) {
return `Invalid JSON provided: ${error.message}`;
} else {
return 'Invalid JSON provided';
}
} }
}; };

View File

@ -571,7 +571,7 @@ class GraphElement {
} }
} catch (e) { } catch (e) {
console.error('flotcharts error', 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; this.ctrl.renderError = true;
} }

View File

@ -61,7 +61,7 @@ function sortSeriesByLabel(s1: { label: string }, s2: { label: string }) {
label1 = parseHistogramLabel(s1.label); label1 = parseHistogramLabel(s1.label);
label2 = parseHistogramLabel(s2.label); label2 = parseHistogramLabel(s2.label);
} catch (err) { } catch (err) {
console.error(err.message || err); console.error(err instanceof Error ? err.message : err);
return 0; return 0;
} }

View File

@ -449,7 +449,7 @@ export class HeatmapRenderer {
return formattedValueToString(v); return formattedValueToString(v);
} }
} catch (err) { } catch (err) {
console.error(err.message || err); console.error(err instanceof Error ? err.message : err);
} }
return value; return value;
}; };

View File

@ -51,8 +51,9 @@ export function prepScatter(
builder = prepConfig(getData, series, theme, ttip); builder = prepConfig(getData, series, theme, ttip);
} catch (e) { } catch (e) {
console.log('prepScatter ERROR', e); console.log('prepScatter ERROR', e);
const errorMessage = e instanceof Error ? e.message : 'Unknown error in prepScatter';
return { return {
error: e.message, error: errorMessage,
series: [], series: [],
}; };
} }

View File

@ -50,9 +50,10 @@ function tryExpectations(received: any[], expected: any[]): jest.CustomMatcherRe
message: () => passMessage(received, expected), message: () => passMessage(received, expected),
}; };
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'An unknown error occurred';
return { return {
pass: false, pass: false,
message: () => err, message: () => message,
}; };
} }
} }

View File

@ -7,7 +7,7 @@
"allowJs": true, "allowJs": true,
"strict": true, "strict": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"useUnknownInCatchVariables": false, "useUnknownInCatchVariables": true,
"incremental": true, "incremental": true,
"tsBuildInfoFile": "./tsconfig.tsbuildinfo" "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
}, },