diff --git a/src/legacy/core_plugins/console/public/kibana.json b/src/legacy/core_plugins/console/public/kibana.json index 3363af353912a..c58a5a90fb9f2 100644 --- a/src/legacy/core_plugins/console/public/kibana.json +++ b/src/legacy/core_plugins/console/public/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["home"] + "requiredPlugins": ["home"], + "optionalPlugins": ["usageCollection"] } diff --git a/src/legacy/core_plugins/console/public/legacy.ts b/src/legacy/core_plugins/console/public/legacy.ts index c456d777187aa..d151a27d27e5c 100644 --- a/src/legacy/core_plugins/console/public/legacy.ts +++ b/src/legacy/core_plugins/console/public/legacy.ts @@ -22,7 +22,13 @@ import { I18nContext } from 'ui/i18n'; import chrome from 'ui/chrome'; import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import { plugin } from './np_ready'; +import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; +import { HomePublicPluginSetup } from '../../../../plugins/home/public'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; + export interface XPluginSet { + usageCollection: UsageCollectionSetup; dev_tools: DevToolsSetup; home: HomePublicPluginSetup; __LEGACY: { @@ -32,10 +38,6 @@ export interface XPluginSet { }; } -import { plugin } from './np_ready'; -import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; -import { HomePublicPluginSetup } from '../../../../plugins/home/public'; - const pluginInstance = plugin({} as any); (async () => { diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx index 5df72c0f03496..0ee7998d331f5 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx @@ -22,6 +22,7 @@ jest.mock('../../../../contexts/editor_context/editor_registry.ts', () => ({ setInputEditor: () => {}, getInputEditor: () => ({ getRequestsInRange: async () => [{ test: 'test' }], + getCoreEditor: () => ({ getCurrentPosition: jest.fn() }), }), }, })); @@ -52,3 +53,6 @@ jest.mock('../../../../models/sense_editor', () => { jest.mock('../../../../hooks/use_send_current_request_to_es/send_request_to_es', () => ({ sendRequestToES: jest.fn(), })); +jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ + getEndpointFromPosition: jest.fn(), +})); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx index 6162397ce0650..73ee6d160613f 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -32,14 +32,18 @@ import { ServicesContextProvider, EditorContextProvider, RequestContextProvider, + ContextValue, } from '../../../../contexts'; +// Mocked functions import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; +import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; + import * as consoleMenuActions from '../console_menu_actions'; import { Editor } from './editor'; describe('Legacy (Ace) Console Editor Component Smoke Test', () => { - let mockedAppContextValue: any; + let mockedAppContextValue: ContextValue; const sandbox = sinon.createSandbox(); const doMount = () => @@ -58,11 +62,15 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { beforeEach(() => { document.queryCommandSupported = sinon.fake(() => true); mockedAppContextValue = { + elasticsearchUrl: 'test', services: { + trackUiMetric: { count: () => {}, load: () => {} }, + settings: {} as any, + storage: {} as any, history: { - getSavedEditorState: () => null, + getSavedEditorState: () => ({} as any), updateCurrentState: jest.fn(), - }, + } as any, notifications: notificationServiceMock.createSetupContract(), }, docLinkVersion: 'NA', @@ -70,10 +78,12 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { }); afterEach(() => { + jest.clearAllMocks(); sandbox.restore(); }); it('calls send current request to ES', async () => { + (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); (sendRequestToES as jest.Mock).mockRejectedValue({}); const editor = doMount(); act(() => { diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts index 797ff5744eec3..2bbe49cd53eac 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts @@ -17,8 +17,7 @@ * under the License. */ -// @ts-ignore -import { getEndpointFromPosition } from '../../../../lib/autocomplete/autocomplete'; +import { getEndpointFromPosition } from '../../../../lib/autocomplete/get_endpoint_from_position'; import { SenseEditor } from '../../../models/sense_editor'; export async function autoIndent(editor: SenseEditor, event: Event) { @@ -40,7 +39,7 @@ export function getDocumentation( } const position = requests[0].range.end; position.column = position.column - 1; - const endpoint = getEndpointFromPosition(editor, position, editor.parser); + const endpoint = getEndpointFromPosition(editor.getCoreEditor(), position, editor.parser); if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) { return endpoint.documentation .replace('/master/', `/${docLinkVersion}/`) diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts index 18234acf15957..e489bd50c9ce0 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { useServicesContext, ServicesContextProvider } from './services_context'; +export { useServicesContext, ServicesContextProvider, ContextValue } from './services_context'; export { useRequestActionContext, diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx index 77f0924a51842..f14685ecd4ac7 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx @@ -20,13 +20,15 @@ import React, { createContext, useContext } from 'react'; import { NotificationsSetup } from 'kibana/public'; import { History, Storage, Settings } from '../../services'; +import { MetricsTracker } from '../../types'; -interface ContextValue { +export interface ContextValue { services: { history: History; storage: Storage; settings: Settings; notifications: NotificationsSetup; + trackUiMetric: MetricsTracker; }; elasticsearchUrl: string; docLinkVersion: string; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 11c1f6638e9cf..10dab65b61d44 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -39,7 +39,8 @@ export interface ESRequestResult { } let CURRENT_REQ_ID = 0; -export function sendRequestToES({ requests }: EsRequestArgs): Promise { +export function sendRequestToES(args: EsRequestArgs): Promise { + const requests = args.requests.slice(); return new Promise((resolve, reject) => { const reqId = ++CURRENT_REQ_ID; const results: ESRequestResult[] = []; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts new file mode 100644 index 0000000000000..4d993512c8fa7 --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts @@ -0,0 +1,40 @@ +/* + * 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 { SenseEditor } from '../../models/sense_editor'; +import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; +import { MetricsTracker } from '../../../types'; + +export const track = (requests: any[], editor: SenseEditor, trackUiMetric: MetricsTracker) => { + const coreEditor = editor.getCoreEditor(); + // `getEndpointFromPosition` gets values from the server-side generated JSON files which + // are a combination of JS, automatically generated JSON and manual overrides. That means + // the metrics reported from here will be tied to the definitions in those files. + // See src/legacy/core_plugins/console/server/api_server/spec + const endpointDescription = getEndpointFromPosition( + coreEditor, + coreEditor.getCurrentPosition(), + editor.parser + ); + + if (requests[0] && endpointDescription) { + const eventName = `${requests[0].method}_${endpointDescription.id ?? 'unknown'}`; + trackUiMetric.count(eventName); + } +}; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index b51c29f8e9db6..6bf0b5024376b 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -19,15 +19,16 @@ import { i18n } from '@kbn/i18n'; import { useCallback } from 'react'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; -import { useServicesContext } from '../../contexts'; +import { useRequestActionContext, useServicesContext } from '../../contexts'; import { sendRequestToES } from './send_request_to_es'; -import { useRequestActionContext } from '../../contexts'; +import { track } from './track'; + // @ts-ignore import mappings from '../../../lib/mappings/mappings'; export const useSendCurrentRequestToES = () => { const { - services: { history, settings, notifications }, + services: { history, settings, notifications, trackUiMetric }, } = useServicesContext(); const dispatch = useRequestActionContext(); @@ -45,9 +46,10 @@ export const useSendCurrentRequestToES = () => { return; } - const results = await sendRequestToES({ - requests, - }); + // Fire and forget + setTimeout(() => track(requests, editor, trackUiMetric), 0); + + const results = await sendRequestToES({ requests }); results.forEach(({ request: { path, method, data } }) => { history.addToHistory(path, method, data); @@ -82,5 +84,5 @@ export const useSendCurrentRequestToES = () => { }); } } - }, [dispatch, settings, history, notifications]); + }, [dispatch, settings, history, notifications, trackUiMetric]); }; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx index 239e4320f00f8..89756513b2b22 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx @@ -22,6 +22,7 @@ import { NotificationsSetup } from 'kibana/public'; import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings, Settings } from '../services'; +import { createUsageTracker } from '../services/tracker'; let settingsRef: Settings; export function legacyBackDoorToSettings() { @@ -36,6 +37,9 @@ export function boot(deps: { }) { const { I18nContext, notifications, docLinkVersion, elasticsearchUrl } = deps; + const trackUiMetric = createUsageTracker(); + trackUiMetric.load('opened_app'); + const storage = createStorage({ engine: window.localStorage, prefix: 'sense:', @@ -50,7 +54,13 @@ export function boot(deps: { value={{ elasticsearchUrl, docLinkVersion, - services: { storage, history, settings, notifications }, + services: { + storage, + history, + settings, + notifications, + trackUiMetric, + }, }} > diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts index 9310de2724fbe..f2102d75685fd 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts @@ -21,3 +21,4 @@ export * from './create'; export * from '../legacy_core_editor/create_readonly'; export { MODE } from '../../../lib/row_parser'; export { SenseEditor } from './sense_editor'; +export { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts index 7520807ca77f5..ac8fa1ea48caa 100644 --- a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts +++ b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts @@ -38,7 +38,6 @@ import { URL_PATH_END_MARKER } from './components/index'; import { createTokenIterator } from '../../application/factories'; import { Position, Token, Range, CoreEditor } from '../../types'; -import { SenseEditor } from '../../application/models/sense_editor'; let LAST_EVALUATED_TOKEN: any = null; @@ -54,11 +53,20 @@ function isUrlParamsToken(token: any) { return false; } } -function getCurrentMethodAndTokenPaths( + +/** + * Get the method and token paths for a specific position in the current editor buffer. + * + * This function can be used for getting autocomplete information or for getting more information + * about the endpoint associated with autocomplete. In future, these concerns should be better + * separated. + * + */ +export function getCurrentMethodAndTokenPaths( editor: CoreEditor, pos: Position, parser: any, - forceEndOfUrl?: boolean + forceEndOfUrl?: boolean /* Flag for indicating whether we want to avoid early escape optimization. */ ) { const tokenIter = createTokenIterator({ editor, @@ -186,7 +194,7 @@ function getCurrentMethodAndTokenPaths( } } - if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0)) { + if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0) && !forceEndOfUrl) { // we had some content and still no path -> the cursor is position after a closed body -> no auto complete return {}; } @@ -298,20 +306,6 @@ function getCurrentMethodAndTokenPaths( } return ret; } -export function getEndpointFromPosition(senseEditor: SenseEditor, pos: Position, parser: any) { - const editor = senseEditor.getCoreEditor(); - const context = { - ...getCurrentMethodAndTokenPaths( - editor, - { column: pos.column, lineNumber: pos.lineNumber }, - parser, - true - ), - }; - const components = getTopLevelUrlCompleteComponents(context.method); - populateContext(context.urlTokenPath, context, editor, true, components); - return context.endpoint; -} // eslint-disable-next-line export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor; parser: any }) { @@ -812,7 +806,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!ret.urlTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid url token path."); return context; } @@ -825,13 +818,11 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor ); if (!context.endpoint) { - // console.log("couldn't resolve an endpoint."); return context; } if (!ret.urlParamsTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid urlParams token path."); return context; } let tokenPath: any[] = []; @@ -859,7 +850,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor context.requestStartRow = ret.requestStartRow; if (!ret.urlTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid url token path."); return context; } @@ -875,7 +865,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!ret.bodyTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid body token path."); return context; } diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts new file mode 100644 index 0000000000000..cb037e29e33f6 --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts @@ -0,0 +1,41 @@ +/* + * 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 { CoreEditor, Position } from '../../types'; +import { getCurrentMethodAndTokenPaths } from './autocomplete'; + +// @ts-ignore +import { getTopLevelUrlCompleteComponents } from '../kb/kb'; +// @ts-ignore +import { populateContext } from './engine'; + +export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: any) { + const lineValue = editor.getLineValue(pos.lineNumber); + const context = { + ...getCurrentMethodAndTokenPaths( + editor, + { column: lineValue.length, lineNumber: pos.lineNumber }, + parser, + true + ), + }; + const components = getTopLevelUrlCompleteComponents(context.method); + populateContext(context.urlTokenPath, context, editor, true, components); + return context.endpoint; +} diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js b/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js index 9012b875e0f2b..e36976fb7acee 100644 --- a/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js +++ b/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js @@ -18,7 +18,6 @@ */ import { stringify as formatQueryString } from 'querystring'; - import $ from 'jquery'; const esVersion = []; diff --git a/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts b/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts new file mode 100644 index 0000000000000..13d5f875b3c6f --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts @@ -0,0 +1,31 @@ +/* + * 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 { METRIC_TYPE } from '@kbn/analytics'; +import { MetricsTracker } from '../types'; +import { createUiStatsReporter } from '../../../../ui_metric/public'; + +const APP_TRACKER_NAME = 'console'; +export const createUsageTracker = (): MetricsTracker => { + const track = createUiStatsReporter(APP_TRACKER_NAME); + return { + count: (eventName: string) => track(METRIC_TYPE.COUNT, eventName), + load: (eventName: string) => track(METRIC_TYPE.LOADED, eventName), + }; +}; diff --git a/src/legacy/core_plugins/console/public/np_ready/types/common.ts b/src/legacy/core_plugins/console/public/np_ready/types/common.ts index ad9ed10d4188f..e44969cd9e80a 100644 --- a/src/legacy/core_plugins/console/public/np_ready/types/common.ts +++ b/src/legacy/core_plugins/console/public/np_ready/types/common.ts @@ -17,6 +17,11 @@ * under the License. */ +export interface MetricsTracker { + count: (eventName: string) => void; + load: (eventName: string) => void; +} + export type BaseResponseType = | 'application/json' | 'text/csv' diff --git a/src/legacy/core_plugins/console/public/np_ready/types/index.ts b/src/legacy/core_plugins/console/public/np_ready/types/index.ts index 9d82237d667b3..78c6b6c8f55cc 100644 --- a/src/legacy/core_plugins/console/public/np_ready/types/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/types/index.ts @@ -20,3 +20,4 @@ export * from './core_editor'; export * from './token'; export * from './tokens_provider'; +export * from './common'; diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json index 843fba30bb489..c78cfeea8473d 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json @@ -19,6 +19,7 @@ "smile" ] }, - "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n" + "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest-overview.html" } }