[lens] show 'View details' UI action to open clusters inspector tab when request fails (#172971)

Closes https://github.com/elastic/kibana/issues/171570

PR make the following changes
1. Consolidates data EsError logic and Lens EsError logic. This resulted
in being able to remove large parts of lens error_helper.tsx file.
2. Consolidates lens WorkspacePanel error logic. Before PR configuration
errors and data loading errors each rendered their own version of
EuiEmptyPrompt with slightly different behavior. Now, both render
WorkspaceErrors component and have the same behavior
3. Updated lens ExpressionWrapper to return original error to embeddable
output.

### Test - EsError in embeddable
1. install sample web logs
2. create new dashboard
3. Click "Create visualization"
4. Drag "timestamp" field into workspace.
5. Click "Save and return"
6. Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```
7. Verify EsError and "View details" action are displayed
<img width="500" alt="Screenshot 2023-12-08 at 1 34 20 PM"
src="1c65b7f3-ece7-4000-a5b2-11127cc38f01">

### Test - multiple configuration errors in lens editor
1. install sample web logs
2. create new lens visualization
3. Drag "timestamp" field into workspace.
4. Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```
5. Verify EsError and "View details" action are displayed
<img width="500" alt="Screenshot 2023-12-08 at 1 09 22 PM"
src="22023512-d344-4f99-abbd-8427d764f821">

### Test - EsError in embeddable
1. install sample web logs
2. create new dashboard
3. Click "Create visualization"
4. Drag "timestamp" field into workspace.
5. Change "Vertical axis" to "Cumulative sum". Select "Field" select and
hit delete key
6. Clone layer one or more times
7. Verify pagination is displayed, allowing users to click through all
configuration errors
<img width="500" alt="Screenshot 2023-12-08 at 12 59 18 PM"
src="6302658a-8cf7-4a1a-a117-ae810c0af539">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-12-20 15:12:54 -07:00 committed by GitHub
parent 5798255638
commit f2ad024082
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 584 additions and 908 deletions

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
export { createEsError } from './src/create_es_error';
export { isEsError, EsError } from './src/es_error';
export { isPainlessError, PainlessError } from './src/painless_error';
export { renderSearchError } from './src/render_search_error';
export type { IEsError } from './src/types';

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EsError should render error message 1`] = `
<div>
<pre>
<code
data-test-subj="errMessage"
>
The supplied interval [2q] could not be parsed as a calendar interval.
</code>
</pre>
</div>
`;

View file

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Painless error should render error message 1`] = `
<div>
<EuiText
data-test-subj="painlessScript"
size="s"
>
Error executing runtime field or scripted field on data view logs
</EuiText>
<EuiSpacer
size="s"
/>
<EuiCodeBlock
data-test-subj="painlessStackTrace"
isCopyable={true}
paddingSize="s"
>
invalid
^---- HERE
</EuiCodeBlock>
<EuiText
data-test-subj="painlessHumanReadableError"
size="s"
>
cannot resolve symbol [invalid]
</EuiText>
</div>
`;

View file

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tsdb error should render error message 1`] = `
<div>
<p
className="eui-textBreakWord"
>
The field [bytes_counter] of Time series type [counter] has been used with the unsupported operation [sum].
</p>
<EuiLink
external={true}
href=""
target="_blank"
>
See more about Time series field types and [counter] supported aggregations
</EuiLink>
</div>
`;

View file

@ -0,0 +1,64 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import type { ApplicationStart, CoreStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import { IEsError } from './types';
import { EsError } from './es_error';
import { PainlessError } from './painless_error';
import { TsdbError } from './tsdb_error';
export interface Services {
application: ApplicationStart;
docLinks: CoreStart['docLinks'];
}
function getNestedCauses(errorCause: estypes.ErrorCause): estypes.ErrorCause[] {
// Give shard failures priority, then try to get the error navigating nested objects
if (errorCause.failed_shards) {
return (errorCause.failed_shards as estypes.ShardFailure[]).map(
(shardFailure) => shardFailure.reason
);
}
return errorCause.caused_by ? getNestedCauses(errorCause.caused_by) : [errorCause];
}
export function createEsError(
err: IEsError,
openInInspector: () => void,
services: Services,
dataView?: DataView
) {
const rootCauses = err.attributes?.error ? getNestedCauses(err.attributes?.error) : [];
const painlessCause = rootCauses.find((errorCause) => {
return errorCause.lang && errorCause.lang === 'painless';
});
if (painlessCause) {
return new PainlessError(err, openInInspector, painlessCause, services.application, dataView);
}
const tsdbCause = rootCauses.find((errorCause) => {
return (
errorCause.type === 'illegal_argument_exception' &&
errorCause.reason &&
/\]\[counter\] is not supported for aggregation/.test(errorCause.reason)
);
});
if (tsdbCause) {
return new TsdbError(err, openInInspector, tsdbCause, services.docLinks);
}
const causeReason = rootCauses[0]?.reason ?? err.attributes?.error?.reason;
const message = causeReason
? causeReason
: i18n.translate('searchErrors.esError.unknownRootCause', { defaultMessage: 'unknown' });
return new EsError(err, message, openInInspector);
}

View file

@ -6,41 +6,31 @@
* Side Public License, v 1.
*/
import { EsError } from './es_error';
import { IEsError } from './types';
import type { ReactElement } from 'react';
import type { CoreStart } from '@kbn/core/public';
import { createEsError } from './create_es_error';
import { renderSearchError } from './render_search_error';
import { shallow } from 'enzyme';
import { coreMock } from '@kbn/core/public/mocks';
const services = {
application: coreMock.createStart().application,
docLinks: {
links: {
fleet: {
datastreamsTSDSMetrics: '',
},
},
} as CoreStart['docLinks'],
};
describe('EsError', () => {
it('contains the same body as the wrapped error', () => {
const error = {
statusCode: 500,
message: 'nope',
attributes: {
error: {
type: 'top_level_exception_type',
reason: 'top-level reason',
},
},
} as IEsError;
const esError = new EsError(error, () => {});
expect(typeof esError.attributes).toEqual('object');
expect(esError.attributes).toEqual(error.attributes);
});
it('contains some explanation of the error in the message', () => {
// error taken from Vega's issue
const error = {
message:
'x_content_parse_exception: [x_content_parse_exception] Reason: [1:78] [date_histogram] failed to parse field [calendar_interval]',
const esError = createEsError(
{
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: {
root_cause: [
{
type: 'x_content_parse_exception',
reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]',
},
],
type: 'x_content_parse_exception',
reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]',
caused_by: {
@ -49,10 +39,27 @@ describe('EsError', () => {
},
},
},
} as IEsError;
const esError = new EsError(error, () => {});
},
() => {},
services
);
test('should set error.message to root "error cause" reason', () => {
expect(esError.message).toEqual(
'EsError: The supplied interval [2q] could not be parsed as a calendar interval.'
'The supplied interval [2q] could not be parsed as a calendar interval.'
);
});
test('should render error message', () => {
const searchErrorDisplay = renderSearchError(esError);
expect(searchErrorDisplay).not.toBeUndefined();
const wrapper = shallow(searchErrorDisplay?.body as ReactElement);
expect(wrapper).toMatchSnapshot();
});
test('should return 1 action', () => {
const searchErrorDisplay = renderSearchError(esError);
expect(searchErrorDisplay).not.toBeUndefined();
expect(searchErrorDisplay?.actions?.length).toBe(1);
});
});

View file

@ -7,11 +7,9 @@
*/
import React from 'react';
import { EuiButton, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ApplicationStart } from '@kbn/core/public';
import { IEsError } from './types';
import { getRootCause } from './utils';
import { EuiButton, EuiCodeBlock } from '@elastic/eui';
import type { IEsError } from './types';
/**
* Checks if a given errors originated from Elasticsearch.
@ -24,39 +22,25 @@ export function isEsError(e: any): e is IEsError {
}
export class EsError extends Error {
readonly attributes: IEsError['attributes'];
public readonly attributes: IEsError['attributes'];
private readonly openInInspector: () => void;
constructor(protected readonly err: IEsError, private readonly openInInspector: () => void) {
super(
`EsError: ${
getRootCause(err?.attributes?.error)?.reason ||
i18n.translate('searchErrors.esError.unknownRootCause', { defaultMessage: 'unknown' })
}`
);
constructor(err: IEsError, message: string, openInInspector: () => void) {
super(message);
this.attributes = err.attributes;
this.openInInspector = openInInspector;
Object.setPrototypeOf(this, new.target.prototype);
}
public getErrorMessage() {
if (!this.attributes?.error) {
return null;
}
const rootCause = getRootCause(this.attributes.error)?.reason;
const topLevelCause = this.attributes.error.reason;
const cause = rootCause ?? topLevelCause;
return (
<>
<EuiSpacer size="s" />
<EuiCodeBlock data-test-subj="errMessage" isCopyable={true} paddingSize="s">
{cause}
</EuiCodeBlock>
</>
<EuiCodeBlock data-test-subj="errMessage" isCopyable={true} paddingSize="s">
{this.message}
</EuiCodeBlock>
);
}
public getActions(application: ApplicationStart) {
public getActions() {
return [
<EuiButton
data-test-subj="viewEsErrorButton"

View file

@ -6,91 +6,86 @@
* Side Public License, v 1.
*/
import type { ReactElement } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import { createEsError } from './create_es_error';
import { renderSearchError } from './render_search_error';
import { shallow } from 'enzyme';
import { coreMock } from '@kbn/core/public/mocks';
const startMock = coreMock.createStart();
import { mount } from 'enzyme';
import { PainlessError } from './painless_error';
import { findTestSubject } from '@elastic/eui/lib/test';
const searchPhaseException = {
error: {
root_cause: [
{
type: 'script_exception',
reason: 'compile error',
script_stack: ['invalid', '^---- HERE'],
script: 'invalid',
lang: 'painless',
position: {
offset: 0,
start: 0,
end: 7,
},
const servicesMock = {
application: coreMock.createStart().application,
docLinks: {
links: {
fleet: {
datastreamsTSDSMetrics: '',
},
],
type: 'search_phase_execution_exception',
reason: 'all shards failed',
phase: 'query',
grouped: true,
failed_shards: [
{
shard: 0,
index: '.kibana_11',
node: 'b3HX8C96Q7q1zgfVLxEsPA',
reason: {
type: 'script_exception',
reason: 'compile error',
script_stack: ['invalid', '^---- HERE'],
script: 'invalid',
lang: 'painless',
position: {
offset: 0,
start: 0,
end: 7,
},
caused_by: {
type: 'illegal_argument_exception',
reason: 'cannot resolve symbol [invalid]',
},
},
},
],
},
status: 400,
},
} as CoreStart['docLinks'],
};
describe('PainlessError', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const dataViewMock = {
title: 'logs',
id: '1234',
} as unknown as DataView;
it('Should show reason and code', () => {
const e = new PainlessError(
{
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: searchPhaseException.error,
describe('Painless error', () => {
const painlessError = createEsError(
{
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: {
type: 'search_phase_execution_exception',
reason: 'all shards failed',
failed_shards: [
{
shard: 0,
index: '.kibana_11',
node: 'b3HX8C96Q7q1zgfVLxEsPA',
reason: {
type: 'script_exception',
reason: 'compile error',
script_stack: ['invalid', '^---- HERE'],
script: 'invalid',
lang: 'painless',
position: {
offset: 0,
start: 0,
end: 7,
},
caused_by: {
type: 'illegal_argument_exception',
reason: 'cannot resolve symbol [invalid]',
},
},
},
],
},
},
() => {}
},
() => {},
servicesMock,
dataViewMock
);
test('should set error.message to painless reason', () => {
expect(painlessError.message).toEqual(
'Error executing runtime field or scripted field on data view logs'
);
const component = mount(e.getErrorMessage());
});
const failedShards = searchPhaseException.error.failed_shards![0];
test('should render error message', () => {
const searchErrorDisplay = renderSearchError(painlessError);
expect(searchErrorDisplay).not.toBeUndefined();
const wrapper = shallow(searchErrorDisplay?.body as ReactElement);
expect(wrapper).toMatchSnapshot();
});
const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode();
const stackTrace = failedShards!.reason.script_stack!.splice(-2).join('\n');
expect(stackTraceElem.textContent).toBe(stackTrace);
const humanReadableError = findTestSubject(
component,
'painlessHumanReadableError'
).getDOMNode();
expect(humanReadableError.textContent).toBe(failedShards?.reason.caused_by?.reason);
const actions = e.getActions(startMock.application);
expect(actions.length).toBe(2);
test('should return 2 actions', () => {
const searchErrorDisplay = renderSearchError(painlessError);
expect(searchErrorDisplay).not.toBeUndefined();
expect(searchErrorDisplay?.actions?.length).toBe(2);
});
});

View file

@ -7,43 +7,58 @@
*/
import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import type { ApplicationStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui';
import type { ApplicationStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { IEsError } from './types';
import { EsError, isEsError } from './es_error';
import { getRootCause } from './utils';
import { EsError } from './es_error';
export class PainlessError extends EsError {
painlessStack?: string;
indexPattern?: DataView;
constructor(err: IEsError, openInInspector: () => void, indexPattern?: DataView) {
super(err, openInInspector);
this.indexPattern = indexPattern;
private readonly applicationStart: ApplicationStart;
private readonly painlessCause: estypes.ErrorCause;
private readonly dataView?: DataView;
constructor(
err: IEsError,
openInInspector: () => void,
painlessCause: estypes.ErrorCause,
applicationStart: ApplicationStart,
dataView?: DataView
) {
super(
err,
i18n.translate('searchErrors.painlessError.painlessScriptedFieldErrorMessage', {
defaultMessage:
'Error executing runtime field or scripted field on data view {indexPatternName}',
values: {
indexPatternName: dataView?.title || '',
},
}),
openInInspector
);
this.applicationStart = applicationStart;
this.painlessCause = painlessCause;
this.dataView = dataView;
}
public getErrorMessage() {
const rootCause = getRootCause(this.err.attributes?.error);
const scriptFromStackTrace = rootCause?.script_stack
? rootCause?.script_stack?.slice(-2).join('\n')
const scriptFromStackTrace = this.painlessCause?.script_stack
? this.painlessCause?.script_stack?.slice(-2).join('\n')
: undefined;
// if the error has been properly processed it will highlight where it occurred.
const hasScript = rootCause?.script_stack?.slice(-1)[0]?.indexOf('HERE') || -1 >= 0;
const humanReadableError = rootCause?.caused_by?.reason;
const hasScript = this.painlessCause?.script_stack?.slice(-1)[0]?.indexOf('HERE') || -1 >= 0;
const humanReadableError = this.painlessCause?.caused_by?.reason;
// fallback, show ES stacktrace
const painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined;
const painlessStack = this.painlessCause?.script_stack
? this.painlessCause?.script_stack.join('\n')
: undefined;
return (
<>
<div>
<EuiText size="s" data-test-subj="painlessScript">
{i18n.translate('searchErrors.painlessError.painlessScriptedFieldErrorMessage', {
defaultMessage:
'Error executing runtime field or scripted field on index pattern {indexPatternName}',
values: {
indexPatternName: this?.indexPattern?.title,
},
})}
{this.message}
</EuiText>
<EuiSpacer size="s" />
{scriptFromStackTrace || painlessStack ? (
@ -56,21 +71,21 @@ export class PainlessError extends EsError {
{humanReadableError}
</EuiText>
) : null}
</>
</div>
);
}
getActions(application: ApplicationStart) {
function onClick(indexPatternId?: string) {
application.navigateToApp('management', {
path: `/kibana/indexPatterns${indexPatternId ? `/patterns/${indexPatternId}` : ''}`,
});
}
const actions = super.getActions(application) ?? [];
getActions() {
const actions = super.getActions() ?? [];
actions.push(
<EuiButtonEmpty
key="editPainlessScript"
onClick={() => onClick(this?.indexPattern?.id)}
onClick={() => () => {
const dataViewId = this.dataView?.id;
this.applicationStart.navigateToApp('management', {
path: `/kibana/indexPatterns${dataViewId ? `/patterns/${dataViewId}` : ''}`,
});
}}
size="s"
>
{i18n.translate('searchErrors.painlessError.buttonTxt', {
@ -81,13 +96,3 @@ export class PainlessError extends EsError {
return actions;
}
}
export function isPainlessError(err: Error | IEsError) {
if (!isEsError(err)) return false;
const rootCause = getRootCause((err as IEsError).attributes?.error);
if (!rootCause) return false;
const { lang } = rootCause;
return lang === 'painless';
}

View file

@ -9,23 +9,18 @@
import { i18n } from '@kbn/i18n';
import { ReactNode } from 'react';
import { BfetchRequestError } from '@kbn/bfetch-error';
import type { ApplicationStart } from '@kbn/core-application-browser';
import { EsError } from './es_error';
export function renderSearchError({
error,
application,
}: {
error: Error;
application: ApplicationStart;
}): { title: string; body: ReactNode; actions?: ReactNode[] } | undefined {
export function renderSearchError(
error: Error
): { title: string; body: ReactNode; actions?: ReactNode[] } | undefined {
if (error instanceof EsError) {
return {
title: i18n.translate('searchErrors.search.esErrorTitle', {
defaultMessage: 'Cannot retrieve search results',
}),
body: error.getErrorMessage(),
actions: error.getActions(application),
actions: error.getActions(),
};
}

View file

@ -0,0 +1,88 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ReactElement } from 'react';
import type { CoreStart } from '@kbn/core/public';
import { createEsError } from './create_es_error';
import { renderSearchError } from './render_search_error';
import { shallow } from 'enzyme';
import { coreMock } from '@kbn/core/public/mocks';
const servicesMock = {
application: coreMock.createStart().application,
docLinks: {
links: {
fleet: {
datastreamsTSDSMetrics: '',
},
},
} as CoreStart['docLinks'],
};
describe('Tsdb error', () => {
const tsdbError = createEsError(
{
statusCode: 400,
message: 'search_phase_execution_exception',
attributes: {
error: {
type: 'status_exception',
reason: 'error while executing search',
caused_by: {
type: 'search_phase_execution_exception',
reason: 'all shards failed',
phase: 'query',
grouped: true,
failed_shards: [
{
shard: 0,
index: 'tsdb_index',
reason: {
type: 'illegal_argument_exception',
reason:
'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]',
},
},
],
caused_by: {
type: 'illegal_argument_exception',
reason:
'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]',
caused_by: {
type: 'illegal_argument_exception',
reason:
'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]',
},
},
},
},
},
},
() => {},
servicesMock
);
test('should set error.message to tsdb reason', () => {
expect(tsdbError.message).toEqual(
'The field [bytes_counter] of Time series type [counter] has been used with the unsupported operation [sum].'
);
});
test('should render error message', () => {
const searchErrorDisplay = renderSearchError(tsdbError);
expect(searchErrorDisplay).not.toBeUndefined();
const wrapper = shallow(searchErrorDisplay?.body as ReactElement);
expect(wrapper).toMatchSnapshot();
});
test('should return 1 actions', () => {
const searchErrorDisplay = renderSearchError(tsdbError);
expect(searchErrorDisplay).not.toBeUndefined();
expect(searchErrorDisplay?.actions?.length).toBe(1);
});
});

View file

@ -0,0 +1,55 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import type { CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { EuiLink } from '@elastic/eui';
import type { IEsError } from './types';
import { EsError } from './es_error';
export class TsdbError extends EsError {
private readonly docLinks: CoreStart['docLinks'];
constructor(
err: IEsError,
openInInspector: () => void,
tsdbCause: estypes.ErrorCause,
docLinks: CoreStart['docLinks']
) {
const [fieldName, _type, _isCounter, opUsed] = tsdbCause.reason!.match(/\[(\w)*\]/g)!;
super(
err,
i18n.translate('searchErrors.tsdbError.message', {
defaultMessage:
'The field {field} of Time series type [counter] has been used with the unsupported operation {op}.',
values: {
field: fieldName,
op: opUsed,
},
}),
openInInspector
);
this.docLinks = docLinks;
}
public getErrorMessage() {
return (
<div>
<p className="eui-textBreakWord">{this.message}</p>
<EuiLink href={this.docLinks.links.fleet.datastreamsTSDSMetrics} external target="_blank">
{i18n.translate('searchErrors.tsdbError.tsdbCounterDocsLabel', {
defaultMessage:
'See more about Time series field types and [counter] supported aggregations',
})}
</EuiLink>
</div>
);
}
}

View file

@ -1,25 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { estypes } from '@elastic/elasticsearch';
function getFailedShardCause(error: estypes.ErrorCause): estypes.ErrorCause | undefined {
const failedShards = error.failed_shards || error.caused_by?.failed_shards;
return failedShards ? failedShards[0]?.reason : undefined;
}
function getNestedCause(error: estypes.ErrorCause): estypes.ErrorCause {
return error.caused_by ? getNestedCause(error.caused_by) : error;
}
export function getRootCause(error?: estypes.ErrorCause): estypes.ErrorCause | undefined {
return error
? // Give shard failures priority, then try to get the error navigating nested objects
getFailedShardCause(error) || getNestedCause(error)
: undefined;
}

View file

@ -20,7 +20,6 @@
"@kbn/core",
"@kbn/kibana-utils-plugin",
"@kbn/data-views-plugin",
"@kbn/core-application-browser",
"@kbn/bfetch-error",
]
}