mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Search] Add telemetry for data plugin search service (#70677)
* [search] Refactor the way search strategies are registered/retrieved on the server * Fix types and tests and update docs * Fix failing test * Fix build of example plugin * Fix functional test * Make server strategies sync * Move strategy name into options * docs * Remove FE strategies * TypeScript of hell delete search explorer * Fix search interceptor OSS tests * typos * test cleanup * Update search interceptor tests and abort utils * [Search] Add telemetry for data plugin search service * Add tracking of average query time * Add tests and rename to collectors * Fix TS * Fixed interceptor jest tests * Add to kibana json * docs * Properly use observables rather than only during setup * Update or create * Swallow version conflict errors Co-authored-by: Liza K <liza.katz@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
fc5bc6b6a2
commit
25d143fdf7
31 changed files with 669 additions and 18 deletions
|
@ -7,7 +7,7 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
setup(core: CoreSetup, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -15,7 +15,7 @@ setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataP
|
|||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| core | <code>CoreSetup</code> | |
|
||||
| { expressions, uiActions } | <code>DataSetupDependencies</code> | |
|
||||
| { expressions, uiActions, usageCollection } | <code>DataSetupDependencies</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -18,4 +18,5 @@ export interface SearchInterceptorDeps
|
|||
| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | <code>CoreStart['http']</code> | |
|
||||
| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | <code>ToastsStart</code> | |
|
||||
| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | <code>CoreStart['uiSettings']</code> | |
|
||||
| [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) | <code>SearchUsageCollector</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md)
|
||||
|
||||
## SearchInterceptorDeps.usageCollector property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
usageCollector?: SearchUsageCollector;
|
||||
```
|
|
@ -14,5 +14,6 @@ export interface ISearchSetup
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | <code>(name: string, strategy: ISearchStrategy) => void</code> | Extension point exposed for other plugins to register their own search strategies. |
|
||||
| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | <code>TRegisterSearchStrategy</code> | Extension point exposed for other plugins to register their own search strategies. |
|
||||
| [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) | <code>SearchUsage</code> | Used internally for telemetry |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) > [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md)
|
||||
|
||||
## ISearchSetup.usage property
|
||||
|
||||
Used internally for telemetry
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
usage: SearchUsage;
|
||||
```
|
|
@ -10,6 +10,7 @@
|
|||
"optionalPlugins": ["usageCollection"],
|
||||
"extraPublicDirs": ["common", "common/utils/abort_utils"],
|
||||
"requiredBundles": [
|
||||
"usageCollection",
|
||||
"kibanaUtils",
|
||||
"kibanaReact",
|
||||
"kibanaLegacy",
|
||||
|
|
|
@ -111,7 +111,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{ expressions, uiActions }: DataSetupDependencies
|
||||
{ expressions, uiActions, usageCollection }: DataSetupDependencies
|
||||
): DataPublicPluginSetup {
|
||||
const startServices = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
|
@ -152,6 +152,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
autocomplete: this.autocomplete.setup(core),
|
||||
search: this.searchService.setup(core, {
|
||||
expressions,
|
||||
usageCollection,
|
||||
getInternalStartServices,
|
||||
packageInfo: this.packageInfo,
|
||||
}),
|
||||
|
|
|
@ -118,6 +118,7 @@ import { KibanaConfigType } from 'src/core/server/kibana_config';
|
|||
import { Location } from 'history';
|
||||
import { LocationDescriptorObject } from 'history';
|
||||
import { MaybePromise } from '@kbn/utility-types';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { MGetParams } from 'elasticsearch';
|
||||
import { MGetResponse } from 'elasticsearch';
|
||||
import { Moment } from 'moment';
|
||||
|
@ -145,6 +146,7 @@ import { RecursiveReadonly } from '@kbn/utility-types';
|
|||
import { ReindexParams } from 'elasticsearch';
|
||||
import { ReindexRethrottleParams } from 'elasticsearch';
|
||||
import { RenderSearchTemplateParams } from 'elasticsearch';
|
||||
import { Reporter } from '@kbn/analytics';
|
||||
import { RequestAdapter } from 'src/plugins/inspector/common';
|
||||
import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common';
|
||||
import { Required } from '@kbn/utility-types';
|
||||
|
@ -1450,7 +1452,7 @@ export class Plugin implements Plugin_2<DataPublicPluginSetup, DataPublicPluginS
|
|||
// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
setup(core: CoreSetup, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
@ -1778,6 +1780,10 @@ export interface SearchInterceptorDeps {
|
|||
toasts: ToastsStart;
|
||||
// (undocumented)
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
usageCollector?: SearchUsageCollector;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
@ -2003,9 +2009,9 @@ export const UI_SETTINGS: {
|
|||
// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:396: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:41:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/types.ts:61:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart } from '../../../../../core/public';
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
import { usageCollectionPluginMock, Setup } from '../../../../usage_collection/public/mocks';
|
||||
import { createUsageCollector } from './create_usage_collector';
|
||||
import { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { from } from 'rxjs';
|
||||
|
||||
describe('Search Usage Collector', () => {
|
||||
let mockCoreSetup: MockedKeys<CoreSetup>;
|
||||
let mockUsageCollectionSetup: Setup;
|
||||
let usageCollector: SearchUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCoreSetup = coreMock.createSetup();
|
||||
(mockCoreSetup as any).getStartServices.mockResolvedValue([
|
||||
{
|
||||
application: {
|
||||
currentAppId$: from(['foo/bar']),
|
||||
},
|
||||
} as jest.Mocked<CoreStart>,
|
||||
{} as any,
|
||||
{} as any,
|
||||
]);
|
||||
mockUsageCollectionSetup = usageCollectionPluginMock.createSetupContract();
|
||||
usageCollector = createUsageCollector(mockCoreSetup, mockUsageCollectionSetup);
|
||||
});
|
||||
|
||||
test('tracks query timeouts', async () => {
|
||||
await usageCollector.trackQueryTimedOut();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][0]).toBe('foo/bar');
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.QUERY_TIMED_OUT
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks query cancellation', async () => {
|
||||
await usageCollector.trackQueriesCancelled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.QUERIES_CANCELLED
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks long popups', async () => {
|
||||
await usageCollector.trackLongQueryPopupShown();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks long popups dismissed', async () => {
|
||||
await usageCollector.trackLongQueryDialogDismissed();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks run query beyond timeout', async () => {
|
||||
await usageCollector.trackLongQueryRunBeyondTimeout();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks response errors', async () => {
|
||||
const duration = 10;
|
||||
await usageCollector.trackError(duration);
|
||||
expect(mockCoreSetup.http.post).toBeCalled();
|
||||
expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage');
|
||||
});
|
||||
|
||||
test('tracks response duration', async () => {
|
||||
const duration = 5;
|
||||
await usageCollector.trackSuccess(duration);
|
||||
expect(mockCoreSetup.http.post).toBeCalled();
|
||||
expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { first } from 'rxjs/operators';
|
||||
import { CoreSetup } from '../../../../../core/public';
|
||||
import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public';
|
||||
import { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types';
|
||||
|
||||
export const createUsageCollector = (
|
||||
core: CoreSetup,
|
||||
usageCollection?: UsageCollectionSetup
|
||||
): SearchUsageCollector => {
|
||||
const getCurrentApp = async () => {
|
||||
const [{ application }] = await core.getStartServices();
|
||||
return application.currentAppId$.pipe(first()).toPromise();
|
||||
};
|
||||
|
||||
return {
|
||||
trackQueryTimedOut: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
SEARCH_EVENT_TYPE.QUERY_TIMED_OUT
|
||||
);
|
||||
},
|
||||
trackQueriesCancelled: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
SEARCH_EVENT_TYPE.QUERIES_CANCELLED
|
||||
);
|
||||
},
|
||||
trackLongQueryPopupShown: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN
|
||||
);
|
||||
},
|
||||
trackLongQueryDialogDismissed: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
currentApp!,
|
||||
METRIC_TYPE.CLICK,
|
||||
SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED
|
||||
);
|
||||
},
|
||||
trackLongQueryRunBeyondTimeout: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
currentApp!,
|
||||
METRIC_TYPE.CLICK,
|
||||
SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT
|
||||
);
|
||||
},
|
||||
trackError: async (duration: number) => {
|
||||
return core.http.post('/api/search/usage', {
|
||||
body: JSON.stringify({
|
||||
eventType: 'error',
|
||||
duration,
|
||||
}),
|
||||
});
|
||||
},
|
||||
trackSuccess: async (duration: number) => {
|
||||
return core.http.post('/api/search/usage', {
|
||||
body: JSON.stringify({
|
||||
eventType: 'success',
|
||||
duration,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
21
src/plugins/data/public/search/collectors/index.ts
Normal file
21
src/plugins/data/public/search/collectors/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createUsageCollector } from './create_usage_collector';
|
||||
export { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types';
|
36
src/plugins/data/public/search/collectors/types.ts
Normal file
36
src/plugins/data/public/search/collectors/types.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum SEARCH_EVENT_TYPE {
|
||||
QUERY_TIMED_OUT = 'queryTimedOut',
|
||||
QUERIES_CANCELLED = 'queriesCancelled',
|
||||
LONG_QUERY_POPUP_SHOWN = 'longQueryPopupShown',
|
||||
LONG_QUERY_DIALOG_DISMISSED = 'longQueryDialogDismissed',
|
||||
LONG_QUERY_RUN_BEYOND_TIMEOUT = 'longQueryRunBeyondTimeout',
|
||||
}
|
||||
|
||||
export interface SearchUsageCollector {
|
||||
trackQueryTimedOut: () => Promise<void>;
|
||||
trackQueriesCancelled: () => Promise<void>;
|
||||
trackLongQueryPopupShown: () => Promise<void>;
|
||||
trackLongQueryDialogDismissed: () => Promise<void>;
|
||||
trackLongQueryRunBeyondTimeout: () => Promise<void>;
|
||||
trackError: (duration: number) => Promise<void>;
|
||||
trackSuccess: (duration: number) => Promise<void>;
|
||||
}
|
|
@ -18,12 +18,13 @@
|
|||
*/
|
||||
|
||||
import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs';
|
||||
import { finalize, filter } from 'rxjs/operators';
|
||||
import { finalize, filter, tap } from 'rxjs/operators';
|
||||
import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public';
|
||||
import { getCombinedSignal, AbortError } from '../../common/utils';
|
||||
import { IEsSearchRequest, IEsSearchResponse } from '../../common/search';
|
||||
import { ISearchOptions } from './types';
|
||||
import { getLongQueryNotification } from './long_query_notification';
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
|
||||
const LONG_QUERY_NOTIFICATION_DELAY = 10000;
|
||||
|
||||
|
@ -32,6 +33,7 @@ export interface SearchInterceptorDeps {
|
|||
application: ApplicationStart;
|
||||
http: CoreStart['http'];
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
usageCollector?: SearchUsageCollector;
|
||||
}
|
||||
|
||||
export class SearchInterceptor {
|
||||
|
@ -121,6 +123,13 @@ export class SearchInterceptor {
|
|||
this.pendingCount$.next(++this.pendingCount);
|
||||
|
||||
return this.runSearch(request, combinedSignal).pipe(
|
||||
tap({
|
||||
next: (e) => {
|
||||
if (this.deps.usageCollector) {
|
||||
this.deps.usageCollector.trackSuccess(e.rawResponse.took);
|
||||
}
|
||||
},
|
||||
}),
|
||||
finalize(() => {
|
||||
this.pendingCount$.next(--this.pendingCount);
|
||||
cleanup();
|
||||
|
@ -185,6 +194,9 @@ export class SearchInterceptor {
|
|||
if (this.longRunningToast) {
|
||||
this.deps.toasts.remove(this.longRunningToast);
|
||||
delete this.longRunningToast;
|
||||
if (this.deps.usageCollector) {
|
||||
this.deps.usageCollector.trackLongQueryDialogDismissed();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -37,9 +37,12 @@ import {
|
|||
getCalculateAutoTimeExpression,
|
||||
} from './aggs';
|
||||
import { ISearchGeneric } from './types';
|
||||
import { SearchUsageCollector, createUsageCollector } from './collectors';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
|
||||
interface SearchServiceSetupDependencies {
|
||||
expressions: ExpressionsSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
getInternalStartServices: GetInternalStartServicesFn;
|
||||
packageInfo: PackageInfo;
|
||||
}
|
||||
|
@ -52,6 +55,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
private esClient?: LegacyApiCaller;
|
||||
private readonly aggTypesRegistry = new AggTypesRegistry();
|
||||
private searchInterceptor!: SearchInterceptor;
|
||||
private usageCollector?: SearchUsageCollector;
|
||||
|
||||
/**
|
||||
* getForceNow uses window.location, so we must have a separate implementation
|
||||
|
@ -62,8 +66,14 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{ expressions, packageInfo, getInternalStartServices }: SearchServiceSetupDependencies
|
||||
{
|
||||
expressions,
|
||||
usageCollection,
|
||||
packageInfo,
|
||||
getInternalStartServices,
|
||||
}: SearchServiceSetupDependencies
|
||||
): ISearchSetup {
|
||||
this.usageCollector = createUsageCollector(core, usageCollection);
|
||||
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
|
||||
|
||||
const aggTypesSetup = this.aggTypesRegistry.setup();
|
||||
|
@ -102,6 +112,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
application: core.application,
|
||||
http: core.http,
|
||||
uiSettings: core.uiSettings,
|
||||
usageCollector: this.usageCollector!,
|
||||
},
|
||||
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
|
||||
);
|
||||
|
@ -134,6 +145,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
types: aggTypesStart,
|
||||
},
|
||||
search,
|
||||
usageCollector: this.usageCollector!,
|
||||
searchSource: {
|
||||
create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies),
|
||||
createEmpty: () => {
|
||||
|
|
|
@ -18,17 +18,22 @@
|
|||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { PackageInfo } from 'kibana/server';
|
||||
import { SearchAggsSetup, SearchAggsStart } from './aggs';
|
||||
import { LegacyApiCaller } from './legacy/es_client';
|
||||
import { SearchInterceptor } from './search_interceptor';
|
||||
import { ISearchSource, SearchSourceFields } from './search_source';
|
||||
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
import {
|
||||
IKibanaSearchRequest,
|
||||
IKibanaSearchResponse,
|
||||
IEsSearchRequest,
|
||||
IEsSearchResponse,
|
||||
} from '../../common/search';
|
||||
import { IndexPatternsContract } from '../../common/index_patterns/index_patterns';
|
||||
import { ExpressionsSetup } from '../../../expressions/public';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
import { GetInternalStartServicesFn } from '../types';
|
||||
|
||||
export interface ISearchOptions {
|
||||
signal?: AbortSignal;
|
||||
|
@ -69,5 +74,19 @@ export interface ISearchStart {
|
|||
create: (fields?: SearchSourceFields) => Promise<ISearchSource>;
|
||||
createEmpty: () => ISearchSource;
|
||||
};
|
||||
usageCollector?: SearchUsageCollector;
|
||||
__LEGACY: ISearchStartLegacy;
|
||||
}
|
||||
|
||||
export { SEARCH_EVENT_TYPE } from './collectors';
|
||||
|
||||
export interface SearchServiceSetupDependencies {
|
||||
expressions: ExpressionsSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
getInternalStartServices: GetInternalStartServicesFn;
|
||||
packageInfo: PackageInfo;
|
||||
}
|
||||
|
||||
export interface SearchServiceStartDependencies {
|
||||
indexPatterns: IndexPatternsContract;
|
||||
}
|
||||
|
|
|
@ -30,10 +30,12 @@ import { QuerySetup, QueryStart } from './query';
|
|||
import { IndexPatternSelectProps } from './ui/index_pattern_select';
|
||||
import { IndexPatternsContract } from './index_patterns';
|
||||
import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar';
|
||||
import { UsageCollectionSetup } from '../../usage_collection/public';
|
||||
|
||||
export interface DataSetupDependencies {
|
||||
expressions: ExpressionsSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export interface DataStartDependencies {
|
||||
|
|
|
@ -82,7 +82,7 @@ export class DataServerPlugin implements Plugin<DataPluginSetup, DataPluginStart
|
|||
core.uiSettings.register(getUiSettings());
|
||||
|
||||
return {
|
||||
search: this.searchService.setup(core),
|
||||
search: this.searchService.setup(core, { usageCollection }),
|
||||
fieldFormats: this.fieldFormats.setup(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,4 +18,5 @@
|
|||
*/
|
||||
export { querySavedObjectType } from './query';
|
||||
export { indexPatternSavedObjectType } from './index_patterns';
|
||||
export { kqlTelemetry } from './kql_telementry';
|
||||
export { kqlTelemetry } from './kql_telemetry';
|
||||
export { searchTelemetry } from './search_telemetry';
|
||||
|
|
29
src/plugins/data/server/saved_objects/search_telemetry.ts
Normal file
29
src/plugins/data/server/saved_objects/search_telemetry.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { SavedObjectsType } from 'kibana/server';
|
||||
|
||||
export const searchTelemetry: SavedObjectsType = {
|
||||
name: 'search-telemetry',
|
||||
namespaceType: 'agnostic',
|
||||
hidden: false,
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {},
|
||||
},
|
||||
};
|
45
src/plugins/data/server/search/collectors/fetch.ts
Normal file
45
src/plugins/data/server/search/collectors/fetch.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { Observable } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { LegacyAPICaller, SharedGlobalConfig } from '../../../../../core/server';
|
||||
import { Usage } from './register';
|
||||
|
||||
export function fetchProvider(config$: Observable<SharedGlobalConfig>) {
|
||||
return async (callCluster: LegacyAPICaller): Promise<Usage> => {
|
||||
const config = await config$.pipe(first()).toPromise();
|
||||
|
||||
const response = await callCluster('search', {
|
||||
index: config.kibana.index,
|
||||
body: {
|
||||
query: { term: { type: { value: 'search-telemetry' } } },
|
||||
},
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
return response.hits.hits.length
|
||||
? (response.hits.hits[0]._source as Usage)
|
||||
: {
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
averageDuration: null,
|
||||
};
|
||||
};
|
||||
}
|
49
src/plugins/data/server/search/collectors/register.ts
Normal file
49
src/plugins/data/server/search/collectors/register.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from 'kibana/server';
|
||||
import { UsageCollectionSetup } from '../../../../usage_collection/server';
|
||||
import { fetchProvider } from './fetch';
|
||||
|
||||
export interface Usage {
|
||||
successCount: number;
|
||||
errorCount: number;
|
||||
averageDuration: number | null;
|
||||
}
|
||||
|
||||
export async function registerUsageCollector(
|
||||
usageCollection: UsageCollectionSetup,
|
||||
context: PluginInitializerContext
|
||||
) {
|
||||
try {
|
||||
const collector = usageCollection.makeUsageCollector<Usage>({
|
||||
type: 'search',
|
||||
isReady: () => true,
|
||||
fetch: fetchProvider(context.config.legacy.globalConfig$),
|
||||
schema: {
|
||||
successCount: { type: 'number' },
|
||||
errorCount: { type: 'number' },
|
||||
averageDuration: { type: 'long' },
|
||||
},
|
||||
});
|
||||
usageCollection.registerCollector(collector);
|
||||
} catch (err) {
|
||||
return; // kibana plugin is not enabled (test environment)
|
||||
}
|
||||
}
|
50
src/plugins/data/server/search/collectors/routes.ts
Normal file
50
src/plugins/data/server/search/collectors/routes.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { CoreSetup } from '../../../../../core/server';
|
||||
import { DataPluginStart } from '../../plugin';
|
||||
import { SearchUsage } from './usage';
|
||||
|
||||
export function registerSearchUsageRoute(
|
||||
core: CoreSetup<object, DataPluginStart>,
|
||||
usage: SearchUsage
|
||||
): void {
|
||||
const router = core.http.createRouter();
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: '/api/search/usage',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
eventType: schema.string(),
|
||||
duration: schema.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, res) => {
|
||||
const { eventType, duration } = request.body;
|
||||
|
||||
if (eventType === 'success') usage.trackSuccess(duration);
|
||||
if (eventType === 'error') usage.trackError(duration);
|
||||
|
||||
return res.ok();
|
||||
}
|
||||
);
|
||||
}
|
77
src/plugins/data/server/search/collectors/usage.ts
Normal file
77
src/plugins/data/server/search/collectors/usage.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { CoreSetup } from 'kibana/server';
|
||||
import { DataPluginStart } from '../../plugin';
|
||||
import { Usage } from './register';
|
||||
|
||||
const SAVED_OBJECT_ID = 'search-telemetry';
|
||||
|
||||
export interface SearchUsage {
|
||||
trackError(duration: number): Promise<void>;
|
||||
trackSuccess(duration: number): Promise<void>;
|
||||
}
|
||||
|
||||
export function usageProvider(core: CoreSetup<object, DataPluginStart>): SearchUsage {
|
||||
const getTracker = (eventType: keyof Usage) => {
|
||||
return async (duration: number) => {
|
||||
const repository = await core
|
||||
.getStartServices()
|
||||
.then(([coreStart]) => coreStart.savedObjects.createInternalRepository());
|
||||
|
||||
let attributes: Usage;
|
||||
let doesSavedObjectExist: boolean = true;
|
||||
|
||||
try {
|
||||
const response = await repository.get<Usage>(SAVED_OBJECT_ID, SAVED_OBJECT_ID);
|
||||
attributes = response.attributes;
|
||||
} catch (e) {
|
||||
doesSavedObjectExist = false;
|
||||
attributes = {
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
averageDuration: 0,
|
||||
};
|
||||
}
|
||||
|
||||
attributes[eventType]++;
|
||||
|
||||
const averageDuration =
|
||||
(duration + (attributes.averageDuration ?? 0)) /
|
||||
((attributes.errorCount ?? 0) + (attributes.successCount ?? 0));
|
||||
|
||||
const newAttributes = { ...attributes, averageDuration };
|
||||
|
||||
try {
|
||||
if (doesSavedObjectExist) {
|
||||
await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, newAttributes);
|
||||
} else {
|
||||
await repository.create(SAVED_OBJECT_ID, newAttributes, { id: SAVED_OBJECT_ID });
|
||||
}
|
||||
} catch (e) {
|
||||
// Version conflict error, swallow
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
trackError: getTracker('errorCount'),
|
||||
trackSuccess: getTracker('successCount'),
|
||||
};
|
||||
}
|
|
@ -34,7 +34,7 @@ describe('Search service', () => {
|
|||
|
||||
describe('setup()', () => {
|
||||
it('exposes proper contract', async () => {
|
||||
const setup = plugin.setup(mockCoreSetup);
|
||||
const setup = plugin.setup(mockCoreSetup, {});
|
||||
expect(setup).toHaveProperty('registerSearchStrategy');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,11 @@ import { ISearchSetup, ISearchStart, ISearchStrategy } from './types';
|
|||
import { registerSearchRoute } from './routes';
|
||||
import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search';
|
||||
import { DataPluginStart } from '../plugin';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/server';
|
||||
import { registerUsageCollector } from './collectors/register';
|
||||
import { usageProvider } from './collectors/usage';
|
||||
import { searchTelemetry } from '../saved_objects';
|
||||
import { registerSearchUsageRoute } from './collectors/routes';
|
||||
import { IEsSearchRequest } from '../../common';
|
||||
|
||||
interface StrategyMap {
|
||||
|
@ -38,15 +43,26 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
|
||||
constructor(private initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup<object, DataPluginStart>): ISearchSetup {
|
||||
public setup(
|
||||
core: CoreSetup<object, DataPluginStart>,
|
||||
{ usageCollection }: { usageCollection?: UsageCollectionSetup }
|
||||
): ISearchSetup {
|
||||
this.registerSearchStrategy(
|
||||
ES_SEARCH_STRATEGY,
|
||||
esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$)
|
||||
);
|
||||
|
||||
registerSearchRoute(core);
|
||||
core.savedObjects.registerType(searchTelemetry);
|
||||
if (usageCollection) {
|
||||
registerUsageCollector(usageCollection, this.initializerContext);
|
||||
}
|
||||
|
||||
return { registerSearchStrategy: this.registerSearchStrategy };
|
||||
const usage = usageProvider(core);
|
||||
|
||||
registerSearchRoute(core);
|
||||
registerSearchUsageRoute(core, usage);
|
||||
|
||||
return { registerSearchStrategy: this.registerSearchStrategy, usage };
|
||||
}
|
||||
|
||||
private search(context: RequestHandlerContext, searchRequest: IEsSearchRequest, options: any) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { RequestHandlerContext } from '../../../../core/server';
|
||||
import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search';
|
||||
import { SearchUsage } from './collectors/usage';
|
||||
import { IEsSearchRequest, IEsSearchResponse } from './es_search';
|
||||
|
||||
export interface ISearchOptions {
|
||||
|
@ -35,6 +36,11 @@ export interface ISearchSetup {
|
|||
* strategies.
|
||||
*/
|
||||
registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void;
|
||||
|
||||
/**
|
||||
* Used internally for telemetry
|
||||
*/
|
||||
usage: SearchUsage;
|
||||
}
|
||||
|
||||
export interface ISearchStart {
|
||||
|
|
|
@ -532,6 +532,8 @@ export interface ISearchOptions {
|
|||
// @public (undocumented)
|
||||
export interface ISearchSetup {
|
||||
registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void;
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchUsage" needs to be exported by the entry point index.d.ts
|
||||
usage: SearchUsage;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
|
|
@ -41,6 +41,7 @@ export class DataEnhancedPlugin
|
|||
application: core.application,
|
||||
http: core.http,
|
||||
uiSettings: core.uiSettings,
|
||||
usageCollector: plugins.data.search.usageCollector,
|
||||
},
|
||||
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
|
||||
);
|
||||
|
|
|
@ -36,12 +36,25 @@ function mockFetchImplementation(responses: any[]) {
|
|||
}
|
||||
|
||||
describe('EnhancedSearchInterceptor', () => {
|
||||
let mockUsageCollector: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCoreStart = coreMock.createStart();
|
||||
|
||||
next.mockClear();
|
||||
error.mockClear();
|
||||
complete.mockClear();
|
||||
jest.clearAllTimers();
|
||||
|
||||
mockUsageCollector = {
|
||||
trackQueryTimedOut: jest.fn(),
|
||||
trackQueriesCancelled: jest.fn(),
|
||||
trackLongQueryPopupShown: jest.fn(),
|
||||
trackLongQueryDialogDismissed: jest.fn(),
|
||||
trackLongQueryRunBeyondTimeout: jest.fn(),
|
||||
trackError: jest.fn(),
|
||||
trackSuccess: jest.fn(),
|
||||
};
|
||||
|
||||
searchInterceptor = new EnhancedSearchInterceptor(
|
||||
{
|
||||
|
@ -49,6 +62,7 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
application: mockCoreStart.application,
|
||||
http: mockCoreStart.http,
|
||||
uiSettings: mockCoreStart.uiSettings,
|
||||
usageCollector: mockUsageCollector,
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
@ -63,6 +77,9 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
is_partial: false,
|
||||
is_running: false,
|
||||
id: 1,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -87,6 +104,9 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
is_partial: false,
|
||||
is_running: true,
|
||||
id: 1,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -95,6 +115,9 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
is_partial: false,
|
||||
is_running: false,
|
||||
id: 1,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -350,6 +373,7 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
([{ signal }]) => signal?.aborted
|
||||
);
|
||||
expect(areAllRequestsAborted).toBe(true);
|
||||
expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -361,6 +385,9 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
is_partial: true,
|
||||
is_running: true,
|
||||
id: 1,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -369,6 +396,9 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
is_partial: false,
|
||||
is_running: false,
|
||||
id: 1,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -427,6 +457,8 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value);
|
||||
expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value);
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1);
|
||||
expect(mockUsageCollector.trackSuccess).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
this.hideToast();
|
||||
this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
if (this.deps.usageCollector) this.deps.usageCollector.trackQueriesCancelled();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -43,6 +44,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
public runBeyondTimeout = () => {
|
||||
this.hideToast();
|
||||
this.timeoutSubscriptions.unsubscribe();
|
||||
if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryRunBeyondTimeout();
|
||||
};
|
||||
|
||||
protected showToast = () => {
|
||||
|
@ -59,6 +61,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
toastLifeTimeMs: 1000000,
|
||||
}
|
||||
);
|
||||
if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryPopupShown();
|
||||
};
|
||||
|
||||
public search(
|
||||
|
@ -85,7 +88,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
}
|
||||
|
||||
// If the response indicates it is complete, stop polling and complete the observable
|
||||
if (!response.is_running) return EMPTY;
|
||||
if (!response.is_running) {
|
||||
if (this.deps.usageCollector && response.rawResponse) {
|
||||
this.deps.usageCollector.trackSuccess(response.rawResponse.took);
|
||||
}
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
id = response.id;
|
||||
// Delay by the given poll interval
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue