[Uptime] Redirect to error page when Heartbeat mappings are missing (#110857)

* Initial PoC of redirect on mapping error is working.

* Update copy. Add comments.

* Include headline element for page title.

* Create mappings for failing functional tests.

* Add functional test for mappings error page.

* Add mapping for certs check.
This commit is contained in:
Justin Kambic 2021-09-23 06:55:43 -04:00 committed by GitHub
parent 6235371cda
commit 26d19e7fd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 272 additions and 16 deletions

View file

@ -17,6 +17,8 @@ export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex';
export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps';
export const MAPPING_ERROR_ROUTE = '/mapping-error';
export enum STATUS {
UP = 'up',
DOWN = 'down',

View file

@ -13,6 +13,7 @@ import { MonitorListComponent } from './monitor_list';
import { useUrlParams } from '../../../hooks';
import { UptimeRefreshContext } from '../../../contexts';
import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts';
import { useMappingCheck } from '../../../hooks/use_mapping_check';
export interface MonitorListProps {
filters?: string;
@ -41,6 +42,7 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
const { lastRefresh } = useContext(UptimeRefreshContext);
const monitorList = useSelector(monitorListSelector);
useMappingCheck(monitorList.error);
useEffect(() => {
dispatch(

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { shouldRedirect } from './use_mapping_check';
describe('useMappingCheck', () => {
describe('should redirect', () => {
it('returns true for appropriate error', () => {
const error = {
request: {},
response: {},
body: {
statusCode: 400,
error: 'Bad Request',
message:
'[search_phase_execution_exception: [illegal_argument_exception] Reason: Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [monitor.id] in order to load field data by uninverting the inverted index. Note that this can use significant memory.]: all shards failed',
},
name: 'Error',
req: {},
res: {},
};
expect(shouldRedirect(error)).toBe(true);
});
it('returns false for undefined', () => {
expect(shouldRedirect(undefined)).toBe(false);
});
it('returns false for missing body', () => {
expect(shouldRedirect({})).toBe(false);
});
it('returns false for incorrect error string', () => {
expect(shouldRedirect({ body: { error: 'not the right type' } })).toBe(false);
});
it('returns false for missing body message', () => {
expect(shouldRedirect({ body: { error: 'Bad Request' } })).toBe(false);
});
it('returns false for incorrect error message', () => {
expect(
shouldRedirect({
body: { error: 'Bad Request', message: 'Not the correct kind of error message' },
})
);
});
});
});

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { MAPPING_ERROR_ROUTE } from '../../common/constants';
interface EsBadRequestError {
body?: {
error?: string;
message?: string;
};
}
function contains(message: string, phrase: string) {
return message.indexOf(phrase) !== -1;
}
export function shouldRedirect(error?: EsBadRequestError) {
if (!error || !error.body || error.body.error !== 'Bad Request' || !error.body.message) {
return false;
}
const { message } = error.body;
return (
contains(message, 'search_phase_execution_exception') ||
contains(message, 'Please use a keyword field instead.') ||
contains(message, 'set fielddata=true')
);
}
export function useMappingCheck(error?: EsBadRequestError) {
const history = useHistory();
useEffect(() => {
if (shouldRedirect(error)) {
history.push(MAPPING_ERROR_ROUTE);
}
}, [error, history]);
}

View file

@ -12,6 +12,7 @@ import { API_URLS } from '../../common/constants';
export enum UptimePage {
Overview = 'Overview',
MappingError = 'MappingError',
Monitor = 'Monitor',
Settings = 'Settings',
Certificates = 'Certificates',

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export { MappingErrorPage } from './mapping_error';
export { MonitorPage } from './monitor';
export { StepDetailPage } from './synthetics/step_detail_page';
export { SettingsPage } from './settings';

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiCode, EuiEmptyPrompt, EuiLink, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { useTrackPageview } from '../../../observability/public';
export const MappingErrorPage = () => {
useTrackPageview({ app: 'uptime', path: 'mapping-error' });
useTrackPageview({ app: 'uptime', path: 'mapping-error', delay: 15000 });
const docLinks = useKibana().services.docLinks;
useBreadcrumbs([
{
text: i18n.translate('xpack.uptime.mappingErrorRoute.breadcrumb', {
defaultMessage: 'Mapping error',
}),
},
]);
return (
<EuiEmptyPrompt
data-test-subj="xpack.uptime.mappingsErrorPage"
iconColor="danger"
iconType="cross"
title={
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.uptime.public.pages.mappingError.title"
defaultMessage="Heartbeat mappings missing"
/>
</h3>
</EuiTitle>
}
body={
<div>
<p>
<FormattedMessage
id="xpack.uptime.public.pages.mappingError.bodyMessage"
defaultMessage="Incorrect mappings detected! Perhaps you forgot to run the heartbeat {setup} command?"
values={{ setup: <EuiCode>setup</EuiCode> }}
/>
</p>
{docLinks && (
<p>
<FormattedMessage
id="xpack.uptime.public.pages.mappingError.bodyDocsLink"
defaultMessage="You can learn how to troubleshoot this issue in the {docsLink}."
values={{
docsLink: (
<EuiLink
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/troubleshoot-uptime-mapping-issues.html`}
target="_blank"
>
docs
</EuiLink>
),
}}
/>
</p>
)}
</div>
}
/>
);
};

View file

@ -11,13 +11,14 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
CERTIFICATES_ROUTE,
MAPPING_ERROR_ROUTE,
MONITOR_ROUTE,
OVERVIEW_ROUTE,
SETTINGS_ROUTE,
STEP_DETAIL_ROUTE,
SYNTHETIC_CHECK_STEPS_ROUTE,
} from '../common/constants';
import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
import { MappingErrorPage, MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
import { CertificatesPage } from './pages/certificates';
import { UptimePage, useUptimeTelemetry } from './hooks';
import { OverviewPageComponent } from './pages/overview';
@ -142,6 +143,26 @@ const Routes: RouteProps[] = [
rightSideItems: [<UptimeDatePicker />],
},
},
{
title: i18n.translate('xpack.uptime.mappingErrorRoute.title', {
defaultMessage: 'Synthetics | mapping error',
}),
path: MAPPING_ERROR_ROUTE,
component: MappingErrorPage,
dataTestSubj: 'uptimeMappingErrorPage',
telemetryId: UptimePage.MappingError,
pageHeader: {
pageTitle: (
<div>
<FormattedMessage
id="xpack.uptime.mappingErrorRoute.pageHeader.title"
defaultMessage="Mapping error"
/>
</div>
),
rightSideItems: [],
},
},
];
const RouteInit: React.FC<Pick<RouteProps, 'path' | 'title' | 'telemetryId'>> = ({

View file

@ -27,7 +27,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
options: {
tags: ['access:uptime-read'],
},
handler: async ({ uptimeEsClient, request }): Promise<any> => {
handler: async ({ uptimeEsClient, request, response }): Promise<any> => {
const { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter, pageSize, query } =
request.query;
@ -35,20 +35,29 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
? JSON.parse(decodeURIComponent(pagination))
: CONTEXT_DEFAULTS.CURSOR_PAGINATION;
const result = await libs.requests.getMonitorStates({
uptimeEsClient,
dateRangeStart,
dateRangeEnd,
pagination: decodedPagination,
pageSize,
filters,
query,
// this is added to make typescript happy,
// this sort of reassignment used to be further downstream but I've moved it here
// because this code is going to be decomissioned soon
statusFilter: statusFilter || undefined,
});
try {
const result = await libs.requests.getMonitorStates({
uptimeEsClient,
dateRangeStart,
dateRangeEnd,
pagination: decodedPagination,
pageSize,
filters,
query,
statusFilter,
});
return result;
return result;
} catch (e) {
/**
* This particular error is usually indicative of a mapping problem within the user's
* indices. It's relevant for the UI because we will be able to provide the user with a
* tailored message to help them remediate this problem on their own with minimal effort.
*/
if (e.name === 'ResponseError') {
return response.badRequest({ body: e });
}
throw e;
}
},
});

View file

@ -9,19 +9,27 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks';
import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls';
const BLANK_INDEX_PATH = 'x-pack/test/functional/es_archives/uptime/blank';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const { uptime } = getPageObjects(['uptime']);
const uptimeService = getService('uptime');
const esArchiver = getService('esArchiver');
const es = getService('es');
describe('certificates', function () {
describe('empty certificates', function () {
before(async () => {
await esArchiver.load(BLANK_INDEX_PATH);
await makeCheck({ es });
await uptime.goToRoot(true);
});
after(async () => {
await esArchiver.unload(BLANK_INDEX_PATH);
});
it('go to certs page', async () => {
await uptimeService.common.waitUntilDataIsLoaded();
await uptimeService.cert.hasViewCertButton();
@ -34,10 +42,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('with certs', function () {
before(async () => {
await esArchiver.load(BLANK_INDEX_PATH);
await makeCheck({ es, tls: true });
await uptime.goToRoot(true);
});
after(async () => {
await esArchiver.unload(BLANK_INDEX_PATH);
});
beforeEach(async () => {
await makeCheck({ es, tls: true });
});

View file

@ -80,5 +80,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./ml_anomaly'));
loadTestFile(require.resolve('./feature_controls'));
});
describe('mappings error state', () => {
loadTestFile(require.resolve('./missing_mappings'));
});
});
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const { common } = getPageObjects(['common']);
const uptimeService = getService('uptime');
const es = getService('es');
describe('missing mappings', function () {
before(async () => {
await makeCheck({ es });
await common.navigateToApp('uptime');
});
it('redirects to mappings error page', async () => {
await uptimeService.common.hasMappingsError();
});
});
};

View file

@ -115,5 +115,8 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider
await testSubjects.missingOrFail('data-missing');
});
},
async hasMappingsError() {
return testSubjects.exists('xpack.uptime.mappingsErrorPage');
},
};
}