[Synthetics] Failed tests by step panel (#149322)

## Summary

Failed tests by step panel

Fixes https://github.com/elastic/kibana/issues/145368

<img width="1523" alt="image"
src="https://user-images.githubusercontent.com/3505601/214053308-458c023e-aaab-4db6-8aa4-a18ce150ce19.png">
This commit is contained in:
Shahzad 2023-01-24 19:22:46 +01:00 committed by GitHub
parent cdc373719a
commit ded5f9a2ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 181 additions and 32 deletions

View file

@ -6,6 +6,7 @@
*/
import { journey, step, before, after } from '@elastic/synthetics';
import moment from 'moment';
import { recordVideo } from '../record_video';
import { createExploratoryViewUrl } from '../../public/components/shared/exploratory_view/configurations/exploratory_view_url';
import { loginToKibana, TIMEOUT_60_SEC, waitForLoadingToFinish } from '../utils';
@ -28,8 +29,8 @@ journey('Exploratory view', async ({ page, params }) => {
{
dataType: 'synthetics',
time: {
from: 'now-10y',
to: 'now',
from: moment().subtract(10, 'y').toISOString(),
to: moment().toISOString(),
},
name: 'synthetics-series-1',
breakdown: 'monitor.type',

View file

@ -22,6 +22,9 @@ interface Props {
lensAttributes: TypedLensByValueInput['attributes'];
setChartTimeRangeContext: Dispatch<SetStateAction<ChartTimeRange | undefined>>;
}
const EXECUTION_CONTEXT = {
type: 'observability_exploratory_view',
};
export function LensEmbeddable(props: Props) {
const { lensAttributes, setChartTimeRangeContext } = props;
@ -92,9 +95,7 @@ export function LensEmbeddable(props: Props) {
attributes={lensAttributes}
onLoad={onLensLoad}
onBrushEnd={onBrushEnd}
executionContext={{
type: 'observability_exploratory_view',
}}
executionContext={EXECUTION_CONTEXT}
/>
{isSaveOpen && lensAttributes && (
<LensSaveModalComponent

View file

@ -36,7 +36,9 @@ export const useEsSearch = <DocumentSource extends unknown, TParams extends esty
{
params,
},
{}
{
legacyHitsTotal: false,
}
)
.subscribe({
next: (result) => {
@ -106,7 +108,7 @@ export const useEsSearch = <DocumentSource extends unknown, TParams extends esty
const { rawResponse } = response as any;
return {
data: rawResponse as ESSearchResponse<DocumentSource, TParams>,
data: rawResponse as ESSearchResponse<DocumentSource, TParams, { restTotalHitsAsInt: false }>,
loading: Boolean(loading),
};
};

View file

@ -89,15 +89,9 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
},
async navigateToAddMonitor() {
if (await page.isVisible('text=select a different monitor type', { timeout: 0 })) {
await page.click('text=select a different monitor type');
} else if (await page.isVisible('text=Create monitor', { timeout: 0 })) {
await page.click('text=Create monitor');
} else {
await page.goto(addMonitor, {
waitUntil: 'networkidle',
});
}
await page.goto(addMonitor, {
waitUntil: 'networkidle',
});
},
async ensureIsOnMonitorConfigPage() {

View file

@ -0,0 +1,92 @@
/*
* 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 { useEsSearch } from '@kbn/observability-plugin/public';
import { useParams } from 'react-router-dom';
import { useMemo } from 'react';
import { createEsQuery } from '../../../../../../common/utils/es_search';
import { Ping } from '../../../../../../common/runtime_types';
import { STEP_END_FILTER } from '../../../../../../common/constants/data_filters';
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants';
import { useSyntheticsRefreshContext } from '../../../contexts';
export function useFailedTestByStep({ to, from }: { to: string; from: string }) {
const { lastRefresh } = useSyntheticsRefreshContext();
const { monitorId } = useParams<{ monitorId: string }>();
const params = createEsQuery({
index: SYNTHETICS_INDEX_PATTERN,
body: {
size: 0,
track_total_hits: true,
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
lte: to,
gte: from,
},
},
},
STEP_END_FILTER,
{
term: {
'synthetics.step.status': 'failed',
},
},
{
term: {
config_id: monitorId,
},
},
],
},
},
aggs: {
steps: {
terms: {
field: 'synthetics.step.name.keyword',
size: 1000,
},
aggs: {
doc: {
top_hits: {
size: 1,
},
},
},
},
},
},
});
const { data, loading } = useEsSearch<Ping, typeof params>(params, [lastRefresh, monitorId], {
name: `getFailedTestsByStep/${monitorId}`,
});
return useMemo(() => {
const total = data?.hits.total.value;
const failedSteps = data?.aggregations?.steps.buckets.map(({ key, doc_count: count, doc }) => {
const index = doc.hits.hits?.[0]._source?.synthetics?.step?.index;
return {
index,
count,
name: key,
percent: (count / total) * 100,
};
});
return {
failedSteps,
loading,
};
}, [data, loading]);
}

View file

@ -19,12 +19,12 @@ import { useHistory, useParams } from 'react-router-dom';
import moment from 'moment';
import { ErrorDetailsLink } from '../../common/links/error_details_link';
import { useSelectedLocation } from '../hooks/use_selected_location';
import { useKibanaDateFormat } from '../../../../../hooks/use_kibana_date_format';
import { Ping, PingState } from '../../../../../../common/runtime_types';
import { useErrorFailedStep } from '../hooks/use_error_failed_step';
import {
formatTestDuration,
formatTestRunAt,
useDateFormatForTest,
} from '../../../utils/monitor_test_result/test_time_formats';
export const ErrorsList = ({
@ -46,7 +46,7 @@ export const ErrorsList = ({
const history = useHistory();
const format = useKibanaDateFormat();
const format = useDateFormatForTest();
const selectedLocation = useSelectedLocation();

View file

@ -8,6 +8,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FailedTestsByStep } from './failed_tests_by_step';
import { PingState } from '../../../../../../common/runtime_types';
import { PanelWithTitle } from '../../common/components/panel_with_title';
import { MonitorErrorsCount } from '../monitor_summary/monitor_errors_count';
@ -61,7 +62,9 @@ export const ErrorsTabContent = ({
</PanelWithTitle>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<PanelWithTitle title={FAILED_TESTS_BY_STEPS_LABEL} />
<PanelWithTitle title={FAILED_TESTS_BY_STEPS_LABEL}>
<FailedTestsByStep time={time} />
</PanelWithTitle>
</EuiFlexItem>
</EuiFlexGroup>
</>

View file

@ -0,0 +1,47 @@
/*
* 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 React, { Fragment } from 'react';
import { EuiProgress, EuiSpacer, EuiLoadingContent } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useFailedTestByStep } from '../hooks/use_failed_tests_by_step';
export const FailedTestsByStep = ({ time }: { time: { to: string; from: string } }) => {
const { failedSteps, loading } = useFailedTestByStep(time);
if (loading && !failedSteps) {
return <EuiLoadingContent lines={3} />;
}
return (
<>
<EuiSpacer size="m" />
<div>
{failedSteps?.map((item) => (
<Fragment key={item.name}>
<EuiProgress
valueText={
<span>
{i18n.translate('xpack.synthetics.monitorDetails.summary.failedTests.count', {
defaultMessage: 'Failed {count}',
values: { count: item.count },
})}
</span>
}
max={100}
color="danger"
size="l"
value={item.percent}
label={`${item.index}. ${item.name}`}
/>
<EuiSpacer size="s" />
</Fragment>
))}
</div>
</>
);
};

View file

@ -36,13 +36,20 @@ export function formatTestRunAt(timestamp: string, format: string) {
return stampedMoment.format(format);
}
export function useFormatTestRunAt(timestamp?: string) {
export function useDateFormatForTest() {
let format = useKibanaDateFormat();
if (!timestamp) {
return '';
}
if (format.endsWith('.SSS')) {
format = format.replace('.SSS', '');
}
return format;
}
export function useFormatTestRunAt(timestamp?: string) {
const format = useDateFormatForTest();
if (!timestamp) {
return '';
}
return formatTestRunAt(timestamp, format);
}

View file

@ -58,7 +58,7 @@ describe('BrowserTestRunResult', function () {
size: 1000,
},
},
{}
{ legacyHitsTotal: false }
);
});

View file

@ -43,7 +43,7 @@ describe('SimpleTestResults', function () {
size: 1000,
},
},
{}
{ legacyHitsTotal: false }
);
});

View file

@ -8,11 +8,14 @@
import { expect, Page } from '@elastic/synthetics';
export async function waitForLoadingToFinish({ page }: { page: Page }) {
while (true) {
let retries = 50;
while (retries) {
retries--;
if ((await page.$(byTestId('kbnLoadingMessage'))) === null) break;
await page.waitForTimeout(2 * 1000);
}
while (true) {
retries = 50;
while (retries) {
if ((await page.$(byTestId('globalLoadingIndicator'))) === null) break;
await page.waitForTimeout(2 * 1000);
}

View file

@ -48,7 +48,7 @@ export function useClientMetricsQuery() {
const backendValue = backEnd.values[pkey] ?? 0;
return {
pageViews: { value: (esQueryResponse.hits.total as any as number) ?? 0 },
pageViews: { value: esQueryResponse.hits.total.value ?? 0 },
totalPageLoadDuration: { value: totalPageLoadDurationValueMs },
backEnd: { value: backendValue },
frontEnd: { value: totalPageLoadDurationValueMs - backendValue },

View file

@ -63,9 +63,9 @@ export function useJsErrorsQuery(pagination: {
return {
totalErrorPages: totalErrorPages?.value ?? 0,
totalErrors: esQueryResponse.hits.total ?? 0,
totalErrors: esQueryResponse.hits.total.value ?? 0,
totalErrorGroups: totalErrorGroups?.value ?? 0,
items: errors?.buckets.map(({ sample, key, impactedPages }: any) => {
items: errors?.buckets.map(({ sample, key, impactedPages }) => {
return {
count: impactedPages.pageCount.value,
errorGroupId: key,

View file

@ -22,8 +22,7 @@ export function formatHasRumResult<T>(
if (!esResult) return esResult;
return {
indices,
// @ts-ignore total.value is undefined by the returned type, total is a `number`
hasData: esResult.hits.total > 0,
hasData: esResult.hits.total.value > 0,
serviceName:
esResult.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
};