[Search] Error notification alignment (#77788) (#78634)

* OSS error alignemnt

* Adjust error messages in xpack

* Add getErrorMessage

* Use showError in vizualize
Add original error to expression exception

* Cleanup

* ts, doc and i18n fixes

* Fix jest tests

* Fix functional test

* functional test

* ts

* Update functional tests

* Add unit tests to interceptor and timeout error

* expose toasts test function

* doc

* typos

* review 1

* Code review

* doc

* doc fix

* visualization type fix

* fix jest

* Fix xpack functional test

* fix xpack test

* code review

* delete debubg flag

* Update texts by @gchaps

* docs and ts

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Liza Katz 2020-09-29 11:13:25 +03:00 committed by GitHub
parent 9b3e54ed03
commit 7918405edc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 849 additions and 465 deletions

View file

@ -19,4 +19,5 @@ export interface ISearchStart
| [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | <code>AggsStart</code> | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) |
| [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | <code>ISearchGeneric</code> | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) |
| [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | <code>ISearchStartSearchSource</code> | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) |
| [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | <code>(e: Error) =&gt; void</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) &gt; [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md)
## ISearchStart.showError property
<b>Signature:</b>
```typescript
showError: (e: Error) => void;
```

View file

@ -19,10 +19,11 @@
| [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | |
| [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | |
| [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | |
| [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) | |
| [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | |
| [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. |
| [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | |
| [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | \* |
| [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) | Request Failure - When an entire multi request fails |
| [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | |
## Enumerations
@ -35,6 +36,7 @@
| [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | |
| [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | |
| [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | |
| [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | |
## Functions

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) &gt; [(constructor)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md)
## PainlessError.(constructor)
Constructs a new instance of the `PainlessError` class
<b>Signature:</b>
```typescript
constructor(err: EsError, request: IKibanaSearchRequest);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| err | <code>EsError</code> | |
| request | <code>IKibanaSearchRequest</code> | |

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) &gt; [getErrorMessage](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md)
## PainlessError.getErrorMessage() method
<b>Signature:</b>
```typescript
getErrorMessage(application: ApplicationStart): JSX.Element;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| application | <code>ApplicationStart</code> | |
<b>Returns:</b>
`JSX.Element`

View file

@ -0,0 +1,30 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md)
## PainlessError class
<b>Signature:</b>
```typescript
export declare class PainlessError extends KbnError
```
## Constructors
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(err, request)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the <code>PainlessError</code> class |
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) | | <code>string</code> | |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md) | | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) &gt; [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md)
## PainlessError.painlessStack property
<b>Signature:</b>
```typescript
painlessStack?: string;
```

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) &gt; [(constructor)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md)
## RequestTimeoutError.(constructor)
Constructs a new instance of the `RequestTimeoutError` class
<b>Signature:</b>
```typescript
constructor(message?: string);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| message | <code>string</code> | |

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md)
## RequestTimeoutError class
Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors.
<b>Signature:</b>
```typescript
export declare class RequestTimeoutError extends Error
```
## Constructors
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(message)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md) | | Constructs a new instance of the <code>RequestTimeoutError</code> class |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [getTimeoutMode](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md)
## SearchInterceptor.getTimeoutMode() method
<b>Signature:</b>
```typescript
protected getTimeoutMode(): TimeoutErrorMode;
```
<b>Returns:</b>
`TimeoutErrorMode`

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [handleSearchError](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md)
## SearchInterceptor.handleSearchError() method
<b>Signature:</b>
```typescript
protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| e | <code>any</code> | |
| request | <code>IKibanaSearchRequest</code> | |
| timeoutSignal | <code>AbortSignal</code> | |
| appAbortSignal | <code>AbortSignal</code> | |
<b>Returns:</b>
`Error`

View file

@ -21,11 +21,13 @@ export declare class SearchInterceptor
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | <code>SearchInterceptorDeps</code> | |
| [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) | | <code>((e: Error) =&gt; void) &amp; import(&quot;lodash&quot;).Cancelable</code> | |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | |
| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | |
| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given <code>search</code> method. Overrides the <code>AbortSignal</code> with one that will abort either when <code>cancelPending</code> is called, when the request times out, or when the original <code>AbortSignal</code> is aborted. Updates <code>pendingCount$</code> when the request is started/finalized. |
| [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | |

View file

@ -9,17 +9,19 @@ Searches using the given `search` method. Overrides the `AbortSignal` with one t
<b>Signature:</b>
```typescript
search(request: IEsSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| request | <code>IEsSearchRequest</code> | |
| request | <code>IKibanaSearchRequest</code> | |
| options | <code>ISearchOptions</code> | |
<b>Returns:</b>
`Observable<IKibanaSearchResponse>`
`Observalbe` emitting the search response or an error.

View file

@ -1,11 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [showError](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md)
## SearchInterceptor.showTimeoutError property
## SearchInterceptor.showError() method
<b>Signature:</b>
```typescript
protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable;
showError(e: Error): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| e | <code>Error</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) &gt; [(constructor)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md)
## SearchTimeoutError.(constructor)
Constructs a new instance of the `SearchTimeoutError` class
<b>Signature:</b>
```typescript
constructor(err: Error, mode: TimeoutErrorMode);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| err | <code>Error</code> | |
| mode | <code>TimeoutErrorMode</code> | |

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) &gt; [getErrorMessage](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md)
## SearchTimeoutError.getErrorMessage() method
<b>Signature:</b>
```typescript
getErrorMessage(application: ApplicationStart): JSX.Element;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| application | <code>ApplicationStart</code> | |
<b>Returns:</b>
`JSX.Element`

View file

@ -0,0 +1,32 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md)
## SearchTimeoutError class
Request Failure - When an entire multi request fails
<b>Signature:</b>
```typescript
export declare class SearchTimeoutError extends KbnError
```
## Constructors
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(err, mode)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md) | | Constructs a new instance of the <code>SearchTimeoutError</code> class |
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md) | | <code>TimeoutErrorMode</code> | |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md) | | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) &gt; [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md)
## SearchTimeoutError.mode property
<b>Signature:</b>
```typescript
mode: TimeoutErrorMode;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md)
## TimeoutErrorMode enum
<b>Signature:</b>
```typescript
export declare enum TimeoutErrorMode
```
## Enumeration Members
| Member | Value | Description |
| --- | --- | --- |
| CHANGE | <code>2</code> | |
| CONTACT | <code>1</code> | |
| UPGRADE | <code>0</code> | |

View file

@ -25,7 +25,7 @@ export const mySearchStrategyProvider = (
): ISearchStrategy<IMyStrategyRequest, IMyStrategyResponse> => {
const es = data.search.getSearchStrategy('es');
return {
search: async (context, request, options) => {
search: async (context, request, options): Promise<IMyStrategyResponse> => {
const esSearchRes = await es.search(context, request, options);
return {
...esSearchRes,

View file

@ -365,8 +365,6 @@ export {
ISearchGeneric,
ISearchSource,
parseSearchSourceJSON,
RequestTimeoutError,
SearchError,
SearchInterceptor,
SearchInterceptorDeps,
SearchRequest,
@ -375,6 +373,11 @@ export {
// expression functions and types
EsdslExpressionFunctionDefinition,
EsRawResponseExpressionTypeDefinition,
// errors
SearchError,
SearchTimeoutError,
TimeoutErrorMode,
PainlessError,
} from './search';
export type { SearchSource } from './search';

View file

@ -8,6 +8,7 @@ import { $Values } from '@kbn/utility-types';
import _ from 'lodash';
import { Action } from 'history';
import { ApiResponse } from '@elastic/elasticsearch/lib/Transport';
import { ApplicationStart } from 'kibana/public';
import { Assign } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import Boom from 'boom';
@ -69,7 +70,6 @@ import { SavedObjectsClientContract } from 'src/core/public';
import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchResponse } from 'elasticsearch';
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
import { Subscription } from 'rxjs';
import { ToastInputFields } from 'src/core/public/notifications';
import { ToastsSetup } from 'kibana/public';
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
@ -1470,6 +1470,8 @@ export interface ISearchStart {
aggs: AggsStart;
search: ISearchGeneric;
searchSource: ISearchStartSearchSource;
// (undocumented)
showError: (e: Error) => void;
}
// @public
@ -1636,6 +1638,19 @@ export interface OptionedValueProp {
value: string;
}
// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "PainlessError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class PainlessError extends KbnError {
// Warning: (ae-forgotten-export) The symbol "EsError" needs to be exported by the entry point index.d.ts
constructor(err: EsError, request: IKibanaSearchRequest);
// (undocumented)
getErrorMessage(application: ApplicationStart): JSX.Element;
// (undocumented)
painlessStack?: string;
}
// Warning: (ae-forgotten-export) The symbol "parseEsInterval" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ParsedInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -1901,13 +1916,6 @@ export interface RefreshInterval {
value: number;
}
// Warning: (ae-missing-release-tag) "RequestTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class RequestTimeoutError extends Error {
constructor(message?: string);
}
// Warning: (ae-missing-release-tag) "SavedQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -2031,24 +2039,27 @@ export class SearchInterceptor {
protected application: CoreStart['application'];
// (undocumented)
protected readonly deps: SearchInterceptorDeps;
// (undocumented)
protected getTimeoutMode(): TimeoutErrorMode;
// (undocumented)
protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error;
// @internal
protected pendingCount$: BehaviorSubject<number>;
// @internal (undocumented)
protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable<IKibanaSearchResponse>;
search(request: IEsSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Observable<IKibanaSearchResponse>;
search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable<IKibanaSearchResponse>;
// @internal (undocumented)
protected setupAbortSignal({ abortSignal, timeout, }: {
abortSignal?: AbortSignal;
timeout?: number;
}): {
combinedSignal: AbortSignal;
timeoutSignal: AbortSignal;
cleanup: () => void;
};
// (undocumented)
protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable;
// @internal
protected timeoutSubscriptions: Subscription;
}
showError(e: Error): void;
}
// Warning: (ae-missing-release-tag) "SearchInterceptorDeps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -2161,6 +2172,17 @@ export interface SearchSourceFields {
version?: boolean;
}
// Warning: (ae-missing-release-tag) "SearchTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class SearchTimeoutError extends KbnError {
constructor(err: Error, mode: TimeoutErrorMode);
// (undocumented)
getErrorMessage(application: ApplicationStart): JSX.Element;
// (undocumented)
mode: TimeoutErrorMode;
}
// Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -2233,6 +2255,18 @@ export class TimeHistory {
// @public (undocumented)
export type TimeHistoryContract = PublicMethodsOf<TimeHistory>;
// Warning: (ae-missing-release-tag) "TimeoutErrorMode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum TimeoutErrorMode {
// (undocumented)
CHANGE = 2,
// (undocumented)
CONTACT = 1,
// (undocumented)
UPGRADE = 0
}
// Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -2322,21 +2356,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -17,4 +17,5 @@
* under the License.
*/
import './fetch_error';
export * from './painless_error';
export * from './timeout_error';

View file

@ -0,0 +1,89 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ApplicationStart } from 'kibana/public';
import { KbnError } from '../../../../kibana_utils/common';
import { EsError, isEsError } from './types';
import { IKibanaSearchRequest } from '..';
export class PainlessError extends KbnError {
painlessStack?: string;
constructor(err: EsError, request: IKibanaSearchRequest) {
const rootCause = getRootCause(err as EsError);
super(
i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', {
defaultMessage: "Error executing Painless script: '{script}'.",
values: { script: rootCause?.script },
})
);
this.painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined;
}
public getErrorMessage(application: ApplicationStart) {
function onClick() {
application.navigateToApp('management', {
path: `/kibana/indexPatterns`,
});
}
return (
<>
{this.message}
<EuiSpacer size="s" />
<EuiSpacer size="s" />
{this.painlessStack ? (
<EuiCodeBlock data-test-subj="painlessStackTrace" isCopyable={true} paddingSize="s">
{this.painlessStack}
</EuiCodeBlock>
) : null}
<EuiText textAlign="right">
<EuiButton color="danger" onClick={onClick} size="s">
<FormattedMessage id="data.painlessError.buttonTxt" defaultMessage="Edit script" />
</EuiButton>
</EuiText>
</>
);
}
}
function getFailedShards(err: EsError) {
const failedShards =
err.body?.attributes?.error?.failed_shards ||
err.body?.attributes?.error?.caused_by?.failed_shards;
return failedShards ? failedShards[0] : undefined;
}
function getRootCause(err: EsError) {
return getFailedShards(err)?.reason;
}
export function isPainlessError(err: Error | EsError) {
if (!isEsError(err)) return false;
const rootCause = getRootCause(err as EsError);
if (!rootCause) return false;
const { lang } = rootCause;
return lang === 'painless';
}

View file

@ -0,0 +1,62 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SearchTimeoutError, TimeoutErrorMode } from './timeout_error';
import { coreMock } from '../../../../../core/public/mocks';
const startMock = coreMock.createStart();
import { mount } from 'enzyme';
import { AbortError } from 'src/plugins/data/common';
describe('SearchTimeoutError', () => {
beforeEach(() => {
jest.clearAllMocks();
startMock.application.navigateToApp.mockImplementation(jest.fn());
});
it('Should navigate to upgrade', () => {
const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.UPGRADE);
const component = mount(e.getErrorMessage(startMock.application));
expect(component.find('EuiButton').length).toBe(1);
component.find('EuiButton').simulate('click');
expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', {
path: '/kibana/indexPatterns',
});
});
it('Should create contact admin message', () => {
const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CONTACT);
const component = mount(e.getErrorMessage(startMock.application));
expect(component.find('EuiButton').length).toBe(0);
});
it('Should navigate to settings', () => {
const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CHANGE);
const component = mount(e.getErrorMessage(startMock.application));
expect(component.find('EuiButton').length).toBe(1);
component.find('EuiButton').simulate('click');
expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', {
path: '/kibana/settings',
});
});
});

View file

@ -0,0 +1,111 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
import { ApplicationStart } from 'kibana/public';
import { KbnError } from '../../../../kibana_utils/common';
export enum TimeoutErrorMode {
UPGRADE,
CONTACT,
CHANGE,
}
/**
* Request Failure - When an entire multi request fails
* @param {Error} err - the Error that came back
*/
export class SearchTimeoutError extends KbnError {
public mode: TimeoutErrorMode;
constructor(err: Error, mode: TimeoutErrorMode) {
super(`Request timeout: ${JSON.stringify(err?.message)}`);
this.mode = mode;
}
private getMessage() {
switch (this.mode) {
case TimeoutErrorMode.UPGRADE:
return i18n.translate('data.search.upgradeLicense', {
defaultMessage:
'Your query has timed out. With our free Basic tier, your queries never time out.',
});
case TimeoutErrorMode.CONTACT:
return i18n.translate('data.search.timeoutContactAdmin', {
defaultMessage:
'Your query has timed out. Contact your system administrator to increase the run time.',
});
case TimeoutErrorMode.CHANGE:
return i18n.translate('data.search.timeoutIncreaseSetting', {
defaultMessage:
'Your query has timed out. Increase run time with the search timeout advanced setting.',
});
}
}
private getActionText() {
switch (this.mode) {
case TimeoutErrorMode.UPGRADE:
return i18n.translate('data.search.upgradeLicenseActionText', {
defaultMessage: 'Upgrade now',
});
break;
case TimeoutErrorMode.CHANGE:
return i18n.translate('data.search.timeoutIncreaseSettingActionText', {
defaultMessage: 'Edit setting',
});
break;
}
}
private onClick(application: ApplicationStart) {
switch (this.mode) {
case TimeoutErrorMode.UPGRADE:
application.navigateToApp('management', {
path: `/kibana/indexPatterns`,
});
break;
case TimeoutErrorMode.CHANGE:
application.navigateToApp('management', {
path: `/kibana/settings`,
});
break;
}
}
public getErrorMessage(application: ApplicationStart) {
const actionText = this.getActionText();
return (
<>
{this.getMessage()}
{actionText && (
<>
<EuiSpacer size="s" />
<EuiText textAlign="right">
<EuiButton color="danger" onClick={() => this.onClick(application)} size="s">
{actionText}
</EuiButton>
</EuiText>
</>
)}
</>
);
}
}

View file

@ -17,9 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
interface FailedShards {
interface FailedShard {
shard: number;
index: string;
node: string;
@ -41,7 +39,7 @@ interface FailedShards {
};
}
interface EsError {
export interface EsError {
body: {
statusCode: number;
error: string;
@ -56,51 +54,20 @@ interface EsError {
];
type: string;
reason: string;
failed_shards: FailedShard[];
caused_by: {
type: string;
reason: string;
phase: string;
grouped: boolean;
failed_shards: FailedShards[];
failed_shards: FailedShard[];
script_stack: string[];
};
};
};
};
}
export function getCause(error: EsError) {
const cause = error.body?.attributes?.error?.root_cause;
if (cause) {
return cause[0];
}
const failedShards = error.body?.attributes?.error?.caused_by?.failed_shards;
if (failedShards && failedShards[0] && failedShards[0].reason) {
return error.body?.attributes?.error?.caused_by?.failed_shards[0].reason;
}
}
export function getPainlessError(error: EsError) {
const cause = getCause(error);
if (!cause) {
return;
}
const { lang, script } = cause;
if (lang !== 'painless') {
return;
}
return {
lang,
script,
message: i18n.translate('discover.painlessError.painlessScriptedFieldErrorMessage', {
defaultMessage: "Error with Painless scripted field '{script}'.",
values: { script },
}),
error: error.body?.message,
};
export function isEsError(e: any): e is EsError {
return !!e.body?.attributes;
}

View file

@ -46,4 +46,4 @@ export {
export { getEsPreference } from './es_search';
export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor';
export { RequestTimeoutError } from './request_timeout_error';
export * from './errors';

View file

@ -34,6 +34,7 @@ function createStartContract(): jest.Mocked<ISearchStart> {
return {
aggs: searchAggsStartMock(),
search: jest.fn(),
showError: jest.fn(),
searchSource: searchSourceMock,
};
}

View file

@ -1,30 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Class used to signify that a request timed out. Useful for applications to conditionally handle
* this type of error differently than other errors.
*/
export class RequestTimeoutError extends Error {
constructor(message = 'Request timed out') {
super(message);
this.message = message;
this.name = 'RequestTimeoutError';
}
}

View file

@ -22,6 +22,7 @@ import { coreMock } from '../../../../core/public/mocks';
import { IEsSearchRequest } from '../../common/search';
import { SearchInterceptor } from './search_interceptor';
import { AbortError } from '../../common';
import { SearchTimeoutError, PainlessError } from './errors';
let searchInterceptor: SearchInterceptor;
let mockCoreSetup: MockedKeys<CoreSetup>;
@ -53,8 +54,8 @@ describe('SearchInterceptor', () => {
expect(result).toBe(mockResponse);
});
test('Observable should fail if fetch has an error', async () => {
const mockResponse: any = { result: 500 };
test('Observable should fail if fetch has an internal error', async () => {
const mockResponse: any = { result: 500, message: 'Internal Error' };
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
@ -68,64 +69,83 @@ describe('SearchInterceptor', () => {
}
});
test('Observable should fail if fetch times out (test merged signal)', async () => {
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
return new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new AbortError());
});
setTimeout(resolve, 5000);
});
});
test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => {
const mockResponse: any = {
result: 500,
body: {
message: 'Request timed out',
},
};
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
const next = jest.fn();
const error = (e: any) => {
expect(next).not.toBeCalled();
expect(e).toBeInstanceOf(AbortError);
};
response.subscribe({ next, error });
jest.advanceTimersByTime(5000);
await flushPromises();
try {
await response.toPromise();
} catch (e) {
expect(e).toBeInstanceOf(SearchTimeoutError);
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
done();
}
});
test('Should not timeout if requestTimeout is undefined', async () => {
searchInterceptor = new SearchInterceptor({
startServices: mockCoreSetup.getStartServices(),
uiSettings: mockCoreSetup.uiSettings,
http: mockCoreSetup.http,
toasts: mockCoreSetup.notifications.toasts,
});
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
return new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new AbortError());
});
test('Search error should be debounced', async (done) => {
const mockResponse: any = {
result: 500,
body: {
message: 'Request timed out',
},
};
mockCoreSetup.http.fetch.mockRejectedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
try {
await searchInterceptor.search(mockRequest).toPromise();
} catch (e) {
expect(e).toBeInstanceOf(SearchTimeoutError);
try {
await searchInterceptor.search(mockRequest).toPromise();
} catch (e2) {
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
done();
}
}
});
setTimeout(resolve, 5000);
});
});
test('Should throw Painless error on server error with OSS format', async (done) => {
const mockResponse: any = {
result: 500,
body: {
attributes: {
error: {
failed_shards: [
{
reason: {
lang: 'painless',
script_stack: ['a', 'b'],
reason: 'banana',
},
},
],
},
},
},
};
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
expect.assertions(1);
const next = jest.fn();
const complete = () => {
expect(next).toBeCalled();
};
response.subscribe({ next, complete });
jest.advanceTimersByTime(5000);
await flushPromises();
try {
await response.toPromise();
} catch (e) {
expect(e).toBeInstanceOf(PainlessError);
done();
}
});
test('Observable should fail if user aborts (test merged signal)', async () => {

View file

@ -17,29 +17,21 @@
* under the License.
*/
import { trimEnd, debounce } from 'lodash';
import {
BehaviorSubject,
throwError,
timer,
Subscription,
defer,
from,
Observable,
NEVER,
} from 'rxjs';
import { get, trimEnd, debounce } from 'lodash';
import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import {
getCombinedSignal,
AbortError,
IEsSearchRequest,
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchOptions,
ES_SEARCH_STRATEGY,
} from '../../common';
import { SearchUsageCollector } from './collectors';
import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors';
import { toMountPoint } from '../../../kibana_react/public';
export interface SearchInterceptorDeps {
http: CoreSetup['http'];
@ -62,12 +54,6 @@ export class SearchInterceptor {
*/
protected pendingCount$ = new BehaviorSubject(0);
/**
* The subscriptions from scheduling the automatic timeout for each request.
* @internal
*/
protected timeoutSubscriptions: Subscription = new Subscription();
/**
* @internal
*/
@ -84,11 +70,46 @@ export class SearchInterceptor {
});
}
/*
* @returns `TimeoutErrorMode` indicating what action should be taken in case of a request timeout based on license and permissions.
* @internal
*/
protected getTimeoutMode() {
return TimeoutErrorMode.UPGRADE;
}
/*
* @returns `Error` a search service specific error or the original error, if a specific error can't be recognized.
* @internal
*/
protected handleSearchError(
e: any,
request: IKibanaSearchRequest,
timeoutSignal: AbortSignal,
appAbortSignal?: AbortSignal
): Error {
if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') {
// Handle a client or a server side timeout
const err = new SearchTimeoutError(e, this.getTimeoutMode());
// Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors.
this.showTimeoutError(err);
return err;
} else if (appAbortSignal?.aborted) {
// In the case an application initiated abort, throw the existing AbortError.
return e;
} else if (isPainlessError(e)) {
return new PainlessError(e, request);
} else {
return e;
}
}
/**
* @internal
*/
protected runSearch(
request: IEsSearchRequest,
request: IKibanaSearchRequest,
signal: AbortSignal,
strategy?: string
): Observable<IKibanaSearchResponse> {
@ -105,41 +126,6 @@ export class SearchInterceptor {
);
}
/**
* Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort
* either when `cancelPending` is called, when the request times out, or when the original
* `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized.
*/
public search(
request: IEsSearchRequest,
options?: ISearchOptions
): Observable<IKibanaSearchResponse> {
// Defer the following logic until `subscribe` is actually called
return defer(() => {
if (options?.abortSignal?.aborted) {
return throwError(new AbortError());
}
const { combinedSignal, cleanup } = this.setupAbortSignal({
abortSignal: options?.abortSignal,
});
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
catchError((e: any) => {
if (e.body?.attributes?.error === 'Request timed out') {
this.showTimeoutError(e);
}
return throwError(e);
}),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
cleanup();
})
);
});
}
/**
* @internal
*/
@ -156,9 +142,7 @@ export class SearchInterceptor {
const timeout$ = timeout ? timer(timeout) : NEVER;
const subscription = timeout$.subscribe(() => {
timeoutController.abort();
this.showTimeoutError(new AbortError());
});
this.timeoutSubscriptions.add(subscription);
// Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs:
// 1. The user manually aborts (via `cancelPending`)
@ -172,34 +156,95 @@ export class SearchInterceptor {
const combinedSignal = getCombinedSignal(signals);
const cleanup = () => {
this.timeoutSubscriptions.remove(subscription);
subscription.unsubscribe();
};
combinedSignal.addEventListener('abort', cleanup);
return {
combinedSignal,
timeoutSignal,
cleanup,
};
}
// Right now we are debouncing but we will hook this up with background sessions to show only one
// error notification per session.
protected showTimeoutError = debounce(
(e: Error) => {
this.deps.toasts.addError(e, {
/**
* Right now we are throttling but we will hook this up with background sessions to show only one
* error notification per session.
* @internal
*/
private showTimeoutError = debounce(
(e: SearchTimeoutError) => {
this.deps.toasts.addDanger({
title: 'Timed out',
toastMessage: i18n.translate('data.search.upgradeLicense', {
defaultMessage:
'One or more queries timed out. With our free Basic tier, your queries never time out.',
}),
text: toMountPoint(e.getErrorMessage(this.application)),
});
},
60000,
{
leading: true,
}
30000,
{ leading: true, trailing: false }
);
/**
* Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort
* either when `cancelPending` is called, when the request times out, or when the original
* `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized.
*
* @param request
* @options
* @returns `Observalbe` emitting the search response or an error.
*/
public search(
request: IKibanaSearchRequest,
options?: ISearchOptions
): Observable<IKibanaSearchResponse> {
// Defer the following logic until `subscribe` is actually called
return defer(() => {
if (options?.abortSignal?.aborted) {
return throwError(new AbortError());
}
const { timeoutSignal, combinedSignal, cleanup } = this.setupAbortSignal({
abortSignal: options?.abortSignal,
});
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
catchError((e: any) => {
return throwError(
this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)
);
}),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
cleanup();
})
);
});
}
/*
*
*/
public showError(e: Error) {
if (e instanceof AbortError) return;
if (e instanceof SearchTimeoutError) {
// The SearchTimeoutError is shown by the interceptor in getSearchError (regardless of how the app chooses to handle errors)
return;
}
if (e instanceof PainlessError) {
this.deps.toasts.addDanger({
title: 'Search Error',
text: toMountPoint(e.getErrorMessage(this.application)),
});
return;
}
this.deps.toasts.addError(e, {
title: 'Search Error',
});
}
}
export type ISearchInterceptor = PublicMethodsOf<SearchInterceptor>;

View file

@ -111,6 +111,9 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
return {
aggs: this.aggsService.start({ fieldFormats, uiSettings }),
search,
showError: (e: Error) => {
this.searchInterceptor.showError(e);
},
searchSource: {
/**
* creates searchsource based on serialized search source fields

View file

@ -73,6 +73,8 @@ export interface ISearchStart {
* {@link ISearchGeneric}
*/
search: ISearchGeneric;
showError: (e: Error) => void;
/**
* high level search
* {@link ISearchStartSearchSource}

View file

@ -27,6 +27,12 @@ import { i18n } from '@kbn/i18n';
import { getState, splitState } from './discover_state';
import { RequestAdapter } from '../../../../inspector/public';
import {
esFilters,
indexPatterns as indexPatternsUtils,
connectToQueryState,
syncQueryStateWithUrl,
} from '../../../../data/public';
import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public';
import { getSortArray, getSortForSearchSource } from './doc_table';
import { createFixedScroll } from './directives/fixed_scroll';
@ -34,7 +40,6 @@ import * as columnActions from './doc_table/actions/columns';
import indexTemplateLegacy from './discover_legacy.html';
import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel';
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
import { getPainlessError } from './get_painless_error';
import { discoverResponseHandler } from './response_handler';
import {
getRequestInspectorStats,
@ -65,12 +70,7 @@ const {
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs';
import { validateTimeRange } from '../helpers/validate_time_range';
import {
esFilters,
indexPatterns as indexPatternsUtils,
connectToQueryState,
syncQueryStateWithUrl,
} from '../../../../data/public';
import { getIndexPatternId } from '../helpers/get_index_pattern_id';
import { addFatalError } from '../../../../kibana_legacy/public';
import {
@ -786,18 +786,10 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
// If the request was aborted then no need to surface this error in the UI
if (error instanceof Error && error.name === 'AbortError') return;
const fetchError = getPainlessError(error);
$scope.fetchStatus = fetchStatuses.NO_RESULTS;
$scope.rows = [];
if (fetchError) {
$scope.fetchError = fetchError;
} else {
toastNotifications.addError(error, {
title: i18n.translate('discover.errorLoadingData', {
defaultMessage: 'Error loading data',
}),
toastMessage: error.shortMessage || error.body?.message,
});
}
data.search.showError(error);
});
};

View file

@ -31,7 +31,6 @@ import { DiscoverNoResults } from '../angular/directives/no_results';
import { DiscoverUninitialized } from '../angular/directives/uninitialized';
import { DiscoverHistogram } from '../angular/directives/histogram';
import { LoadingSpinner } from './loading_spinner/loading_spinner';
import { DiscoverFetchError, FetchError } from './fetch_error/fetch_error';
import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react';
import { SkipBottomButton } from './skip_bottom_button';
import {
@ -54,7 +53,6 @@ export interface DiscoverLegacyProps {
addColumn: (column: string) => void;
fetch: () => void;
fetchCounter: number;
fetchError: FetchError;
fieldCounts: Record<string, number>;
histogramData: Chart;
hits: number;
@ -95,7 +93,6 @@ export function DiscoverLegacy({
addColumn,
fetch,
fetchCounter,
fetchError,
fieldCounts,
histogramData,
hits,
@ -216,8 +213,7 @@ export function DiscoverLegacy({
{resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />}
{/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/}
<span style={{ display: resultState !== 'loading' ? 'none' : '' }}>
{fetchError && <DiscoverFetchError fetchError={fetchError} />}
<div className="dscOverlay" style={{ display: fetchError ? 'none' : '' }}>
<div className="dscOverlay">
<LoadingSpinner />
</div>
</span>

View file

@ -1,3 +0,0 @@
.discoverFetchError {
max-width: 1000px;
}

View file

@ -1,96 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import './fetch_error.scss';
import React, { Fragment } from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
import { getServices } from '../../../kibana_services';
export interface FetchError {
lang: string;
script: string;
message: string;
error: string;
}
interface Props {
fetchError: FetchError;
}
export const DiscoverFetchError = ({ fetchError }: Props) => {
if (!fetchError) {
return null;
}
let body;
if (fetchError.lang === 'painless') {
const { chrome } = getServices();
const mangagementUrlObj = chrome.navLinks.get('kibana:stack_management');
const managementUrl = mangagementUrlObj ? mangagementUrlObj.url : '';
const url = `${managementUrl}/kibana/indexPatterns`;
body = (
<p>
<FormattedMessage
id="discover.fetchError.howToAddressErrorDescription"
defaultMessage="You can address this error by editing the {fetchErrorScript} field
in {managementLink}, under the {scriptedFields} tab."
values={{
fetchErrorScript: `'${fetchError.script}'`,
scriptedFields: (
<FormattedMessage
id="discover.fetchError.scriptedFieldsText"
defaultMessage="&ldquo;Scripted fields&rdquo;"
/>
),
managementLink: (
<a href={url}>
<FormattedMessage
id="discover.fetchError.managmentLinkText"
defaultMessage="Management &gt; Index Patterns"
/>
</a>
),
}}
/>
</p>
);
}
return (
<I18nProvider>
<Fragment>
<EuiSpacer size="xl" />
<EuiFlexGroup justifyContent="center" data-test-subj="discoverFetchError">
<EuiFlexItem grow={false} className="discoverFetchError">
<EuiCallOut title={fetchError.message} color="danger" iconType="cross">
{body}
<EuiCodeBlock>{fetchError.error}</EuiCodeBlock>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
</Fragment>
</I18nProvider>
);
};

View file

@ -30,6 +30,7 @@ export type ExpressionValueError = ExpressionValueBoxed<
message: string;
name?: string;
stack?: string;
original?: Error;
};
info?: unknown;
}

View file

@ -21,7 +21,7 @@ import { ExpressionValueError } from '../../common';
type ErrorLike = Partial<Pick<Error, 'name' | 'message' | 'stack'>>;
export const createError = (err: string | ErrorLike): ExpressionValueError => ({
export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({
type: 'error',
error: {
stack:
@ -32,5 +32,6 @@ export const createError = (err: string | ErrorLike): ExpressionValueError => ({
: undefined,
message: typeof err === 'string' ? err : String(err.message),
name: typeof err === 'object' ? err.name || 'Error' : 'Error',
original: err instanceof Error ? err : undefined,
},
});

View file

@ -50,6 +50,8 @@ describe('getVisualizationInstance', () => {
};
savedVisMock = {};
// @ts-expect-error
mockServices.data.search.showError.mockImplementation(() => {});
// @ts-expect-error
mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock);
// @ts-expect-error
mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock);
@ -119,6 +121,6 @@ describe('getVisualizationInstance', () => {
error: 'error',
});
expect(mockServices.toastNotifications.addError).toHaveBeenCalled();
expect(mockServices.data.search.showError).toHaveBeenCalled();
});
});

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import {
SerializedVis,
Vis,
@ -28,6 +27,7 @@ import {
import { SearchSourceFields } from 'src/plugins/data/public';
import { SavedObject } from 'src/plugins/saved_objects/public';
import { cloneDeep } from 'lodash';
import { ExpressionValueError } from 'src/plugins/expressions/public';
import { createSavedSearchesLoader } from '../../../../discover/public';
import { VisualizeServices } from '../types';
@ -35,14 +35,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
vis: Vis,
visualizeServices: VisualizeServices
) => {
const {
chrome,
data,
overlays,
createVisEmbeddableFromObject,
savedObjects,
toastNotifications,
} = visualizeServices;
const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices;
const embeddableHandler = (await createVisEmbeddableFromObject(vis, {
timeRange: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
@ -51,11 +44,9 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
embeddableHandler.getOutput$().subscribe((output) => {
if (output.error) {
toastNotifications.addError(output.error, {
title: i18n.translate('visualize.error.title', {
defaultMessage: 'Visualization error',
}),
});
data.search.showError(
((output.error as unknown) as ExpressionValueError['error']).original || output.error
);
}
});

View file

@ -22,7 +22,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const toasts = getService('toasts');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
describe('errors', function describeIndexTests() {
@ -39,8 +39,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('invalid scripted field error', () => {
it('is rendered', async () => {
const isFetchErrorVisible = await testSubjects.exists('discoverFetchError');
expect(isFetchErrorVisible).to.be(true);
const toast = await toasts.getToastElement(1);
const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace');
expect(painlessStackTrace).not.to.be(undefined);
});
});
});

View file

@ -63,7 +63,7 @@ export function ToastsProvider({ getService }: FtrProviderContext) {
}
}
private async getToastElement(index: number) {
public async getToastElement(index: number) {
const list = await this.getGlobalToastList();
return await list.findByCssSelector(`.euiToast:nth-child(${index})`);
}

View file

@ -8,6 +8,7 @@ import { coreMock } from '../../../../../src/core/public/mocks';
import { EnhancedSearchInterceptor } from './search_interceptor';
import { CoreSetup, CoreStart } from 'kibana/public';
import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common';
import { SearchTimeoutError } from 'src/plugins/data/public';
const timeTravel = (msToRun = 0) => {
jest.advanceTimersByTime(msToRun);
@ -265,7 +266,7 @@ describe('EnhancedSearchInterceptor', () => {
await timeTravel(1000);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError);
expect(mockCoreSetup.http.fetch).toHaveBeenCalled();
expect(mockCoreSetup.http.delete).not.toHaveBeenCalled();
});
@ -305,7 +306,7 @@ describe('EnhancedSearchInterceptor', () => {
await timeTravel(1000);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError);
expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreSetup.http.delete).toHaveBeenCalled();
});

View file

@ -5,9 +5,7 @@
*/
import { throwError, EMPTY, timer, from, Subscription } from 'rxjs';
import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators';
import { debounce } from 'lodash';
import { i18n } from '@kbn/i18n';
import { mergeMap, expand, takeUntil, finalize, catchError } from 'rxjs/operators';
import {
SearchInterceptor,
SearchInterceptorDeps,
@ -15,6 +13,7 @@ import {
} from '../../../../../src/plugins/data/public';
import { isErrorResponse, isCompleteResponse } from '../../../../../src/plugins/data/public';
import { AbortError, toPromise } from '../../../../../src/plugins/data/common';
import { TimeoutErrorMode } from '../../../../../src/plugins/data/public';
import { IAsyncSearchOptions } from '.';
import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common';
@ -40,6 +39,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
this.uiSettingsSub.unsubscribe();
}
protected getTimeoutMode() {
return this.application.capabilities.advancedSettings?.save
? TimeoutErrorMode.CHANGE
: TimeoutErrorMode.CONTACT;
}
/**
* Abort our `AbortController`, which in turn aborts any intercepted searches.
*/
@ -55,7 +60,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
) {
let { id } = request;
const { combinedSignal, cleanup } = this.setupAbortSignal({
const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({
abortSignal: options.abortSignal,
timeout: this.searchTimeout,
});
@ -86,15 +91,14 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
);
}),
takeUntil(aborted$),
tap({
error: () => {
// If we haven't received the response to the initial request, including the ID, then
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
this.deps.http.delete(`/internal/search/${strategy}/${id}`);
}
},
catchError((e: any) => {
// If we haven't received the response to the initial request, including the ID, then
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
this.deps.http.delete(`/internal/search/${strategy}/${id}`);
}
return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal));
}),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
@ -102,28 +106,4 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
})
);
}
// Right now we are debouncing but we will hook this up with background sessions to show only one
// error notification per session.
protected showTimeoutError = debounce(
(e: Error) => {
const message = this.application.capabilities.advancedSettings?.save
? i18n.translate('xpack.data.search.timeoutIncreaseSetting', {
defaultMessage:
'One or more queries timed out. Increase run time with the search.timeout advanced setting.',
})
: i18n.translate('xpack.data.search.timeoutContactAdmin', {
defaultMessage:
'One or more queries timed out. Contact your system administrator to increase the run time.',
});
this.deps.toasts.addError(e, {
title: 'Timed out',
toastMessage: message,
});
},
60000,
{
leading: true,
}
);
}

View file

@ -44,6 +44,7 @@ describe('alert actions', () => {
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
searchStrategyClient = {
aggs: {} as ISearchStart['aggs'],
showError: jest.fn(),
search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }),
searchSource: {} as ISearchStart['searchSource'],
};

View file

@ -1370,10 +1370,6 @@
"discover.embeddable.inspectorRequestDataTitle": "データ",
"discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
"discover.embeddable.search.displayName": "検索",
"discover.errorLoadingData": "データの読み込み中にエラーが発生",
"discover.fetchError.howToAddressErrorDescription": "このエラーは、{scriptedFields}タブにある {managementLink}の{fetchErrorScript}フィールドを編集することで解決できます。",
"discover.fetchError.managmentLinkText": "管理>インデックスパターン",
"discover.fetchError.scriptedFieldsText": "「スクリプトフィールド」",
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"",
@ -1450,7 +1446,6 @@
"discover.notifications.invalidTimeRangeTitle": "無効な時間範囲",
"discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。",
"discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。",
"discover.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトのフィールド「{script}」のエラー.",
"discover.reloadSavedSearchButton": "検索をリセット",
"discover.rootBreadcrumb": "発見",
"discover.savedSearch.savedObjectName": "保存検索",
@ -4385,7 +4380,6 @@
"visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPatternまたはsavedSearchIdが必要です",
"visualize.createVisualization.noVisTypeErrorMessage": "有効なビジュアライゼーションタイプを指定してください",
"visualize.editor.createBreadcrumb": "作成",
"visualize.error.title": "ビジュアライゼーションエラー",
"visualize.helpMenu.appName": "可視化",
"visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました",
"visualize.listing.betaTitle": "ベータ",

View file

@ -1371,10 +1371,6 @@
"discover.embeddable.inspectorRequestDataTitle": "数据",
"discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。",
"discover.embeddable.search.displayName": "搜索",
"discover.errorLoadingData": "加载数据时出错",
"discover.fetchError.howToAddressErrorDescription": "您可以通过编辑{managementLink}中{scriptedFields}选项卡下的“{fetchErrorScript}”字段来解决此错误。",
"discover.fetchError.managmentLinkText": "“管理”>“索引模式”",
"discover.fetchError.scriptedFieldsText": "“脚本字段”",
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”",
@ -1451,7 +1447,6 @@
"discover.notifications.invalidTimeRangeTitle": "时间范围无效",
"discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。",
"discover.notifications.savedSearchTitle": "搜索“{savedSearchTitle}”已保存",
"discover.painlessError.painlessScriptedFieldErrorMessage": "Painless 脚本字段“{script}”有错误。",
"discover.reloadSavedSearchButton": "重置搜索",
"discover.rootBreadcrumb": "Discover",
"discover.savedSearch.savedObjectName": "已保存搜索",
@ -4386,7 +4381,6 @@
"visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId",
"visualize.createVisualization.noVisTypeErrorMessage": "必须提供有效的可视化类型",
"visualize.editor.createBreadcrumb": "创建",
"visualize.error.title": "可视化错误",
"visualize.helpMenu.appName": "Visualize",
"visualize.linkedToSearch.unlinkSuccessNotificationText": "已取消与已保存搜索“{searchTitle}”的链接",
"visualize.listing.betaTitle": "公测版",

View file

@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const toasts = getService('toasts');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
describe('errors', function describeIndexTests() {
@ -23,11 +23,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
after(async function () {
await esArchiver.unload('invalid_scripted_field');
});
// this is the same test as in OSS but it catches different error message issue in different licences
describe('invalid scripted field error', () => {
it('is rendered', async () => {
const isFetchErrorVisible = await testSubjects.exists('discoverFetchError');
expect(isFetchErrorVisible).to.be(true);
const toast = await toasts.getToastElement(1);
const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace');
expect(painlessStackTrace).not.to.be(undefined);
});
});
});