From ced7ff924fc199085b2e97f16326dc0c464cafd7 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Tue, 4 Feb 2025 18:10:57 +0300 Subject: [PATCH] chore(Queries): telemetry [YTFRONT-4612] --- .../ui/src/server/components/layout-config.ts | 4 +- .../src/server/controllers/neuralNetwork.ts | 36 +++++++--- packages/ui/src/server/routes.ts | 3 +- packages/ui/src/shared/neuralNetwork.ts | 30 ++++++++- .../helpers/createInlineSuggestions.ts | 65 ++++++++++--------- .../monaco-yql-languages/neuralNetwork/api.ts | 29 +++++++++ .../neuralNetwork/useMonacoNeuralNetwork.ts | 46 +++++++++++++ .../query-tracker/QueryEditor/QueryEditor.tsx | 8 ++- 8 files changed, 175 insertions(+), 46 deletions(-) create mode 100644 packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/api.ts create mode 100644 packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/useMonacoNeuralNetwork.ts diff --git a/packages/ui/src/server/components/layout-config.ts b/packages/ui/src/server/components/layout-config.ts index 1306b15a3..ded9d432c 100644 --- a/packages/ui/src/server/components/layout-config.ts +++ b/packages/ui/src/server/components/layout-config.ts @@ -71,7 +71,9 @@ export async function getLayoutConfig(req: Request, params: Params): Promise { - try { - const nNetwork = ServerFactory.createNeuralNetworkApi( - req.ctx.config.uiSettings.neuralNetworkConfig, - ); +const getNeuralNetwork = (req: Request) => { + const nNetwork = ServerFactory.createNeuralNetworkApi( + req.ctx.config.uiSettings.neuralNetworkConfig, + ); - if (!nNetwork) { - throw new ErrorWithCode(500, 'Neural network is not configured'); - } + if (!nNetwork) { + throw new ErrorWithCode(500, 'Neural network is not configured'); + } - const suggestions = await nNetwork.getQuerySuggestions(req); - res.status(200).json({items: suggestions}); + return nNetwork; +}; + +export const getQuerySuggestions = async (req: Request, res: Response) => { + try { + const nNetwork = getNeuralNetwork(req); + const data = await nNetwork.getQuerySuggestions(req); + res.status(200).json(data); } catch (e) { req.ctx.logError('Query suggestions error', e); sendApiError(res, e); } }; + +export const sendTelemetry = async (req: Request, res: Response) => { + try { + const nNetwork = getNeuralNetwork(req); + await nNetwork.sendTelemetry(req); + res.status(200).json({success: true}); + } catch (e) { + req.ctx.logError('Telemetry error', e); + sendApiError(res, e); + } +}; diff --git a/packages/ui/src/server/routes.ts b/packages/ui/src/server/routes.ts index 9ded76166..3da8c658e 100644 --- a/packages/ui/src/server/routes.ts +++ b/packages/ui/src/server/routes.ts @@ -34,7 +34,7 @@ import { removeToken, } from './controllers/vcs'; import {ytTabletErrorsApi} from './controllers/yt-tablet-errors-api'; -import {getQuerySuggestions} from './controllers/neuralNetwork'; +import {getQuerySuggestions, sendTelemetry} from './controllers/neuralNetwork'; const HOME_INDEX_TARGET: AppRouteDescription = {handler: homeIndexFactory(), ui: true}; @@ -64,6 +64,7 @@ const routes: AppRoutes = { 'GET /api/vcs/tokens-availability': {handler: getVcsTokensAvailability}, 'GET /api/neural-network/query-suggestions': {handler: getQuerySuggestions}, + 'POST /api/neural-network/send-telemetry': {handler: sendTelemetry}, 'POST /api/yt/:ytAuthCluster/change-password': {handler: handleChangePassword, ui: true}, 'POST /api/remote-copy': {handler: handleRemoteCopy}, diff --git a/packages/ui/src/shared/neuralNetwork.ts b/packages/ui/src/shared/neuralNetwork.ts index a8dfc71ac..ef8c01dd4 100644 --- a/packages/ui/src/shared/neuralNetwork.ts +++ b/packages/ui/src/shared/neuralNetwork.ts @@ -1,5 +1,33 @@ import {Request} from '@gravity-ui/expresskit'; export interface NeuralNetworkApi { - getQuerySuggestions(req: Request): Promise; + getQuerySuggestions(req: Request): Promise<{items: string[]; requestId: string}>; + sendTelemetry(req: Request): Promise; } + +export type TelemetryData = { + requestId: string; + timestamp: number; +}; + +export type AcceptedTelemetryData = TelemetryData & { + type: 'accepted'; + acceptedText: string; + convertedText: string; +}; + +export type DiscardedTelemetryData = TelemetryData & { + type: 'discarded'; + reason: 'OnCancel'; + discardedText: string; +}; + +export type IgnoredTelemetryData = TelemetryData & { + type: 'ignored'; + ignoredText: string; +}; + +export type NeuralNetworkTelemetryData = + | AcceptedTelemetryData + | DiscardedTelemetryData + | IgnoredTelemetryData; diff --git a/packages/ui/src/ui/libs/monaco-yql-languages/helpers/createInlineSuggestions.ts b/packages/ui/src/ui/libs/monaco-yql-languages/helpers/createInlineSuggestions.ts index b20527250..7ba11fda7 100644 --- a/packages/ui/src/ui/libs/monaco-yql-languages/helpers/createInlineSuggestions.ts +++ b/packages/ui/src/ui/libs/monaco-yql-languages/helpers/createInlineSuggestions.ts @@ -1,32 +1,19 @@ import {CancellationToken, Position, editor, languages} from 'monaco-editor'; import {getRangeToInsertSuggestion} from './getRangeToInsertSuggestion'; -import axios from 'axios'; import {QueryEngine} from '../../../pages/query-tracker/module/engines'; import debounce_ from 'lodash/debounce'; import {getWindowStore} from '../../../store/window-store'; import {getConfigData} from '../../../config/ui-settings'; +import {getNeuralNetworkSuggestions} from '../neuralNetwork/api'; +import { + setRequestId, + setSuggestions, +} from '../../../pages/query-tracker/module/neuralNetwork/neuralNetworkSlice'; +import {selectNeuralNetworkContextId} from '../../../pages/query-tracker/module/neuralNetwork/selectors'; +import {AcceptedTelemetryData} from '../../../../shared/neuralNetwork'; -const getSuggestions = async (data: { - contextId: string; - query: string; - line: number; - column: number; - engine: QueryEngine; -}) => { - try { - const response = await axios.get<{items: string[]}>( - '/api/neural-network/query-suggestions', - { - params: data, - }, - ); - return response.data.items; - } catch (e) { - return []; - } -}; - -const debouncedGetSuggestions = debounce_(getSuggestions, 200); +const debouncedGetSuggestions = debounce_(getNeuralNetworkSuggestions, 200); +const store = getWindowStore(); export const createInlineSuggestions = (engine: QueryEngine) => @@ -36,7 +23,7 @@ export const createInlineSuggestions = _context: languages.InlineCompletionContext, _token: CancellationToken, ): Promise<{items: languages.InlineCompletion[]}> => { - const state = getWindowStore().getState(); + const contextId = selectNeuralNetworkContextId(store.getState()); const hasNeuralNetwork = getConfigData().neuralNetwork; if (!hasNeuralNetwork) { @@ -45,25 +32,43 @@ export const createInlineSuggestions = }; } - const suggestions = await debouncedGetSuggestions({ - contextId: state.queryTracker.query.draft.annotations?.contextId || '', + const response = await debouncedGetSuggestions({ + contextId, query: model.getValue(), line: monacoCursorPosition.lineNumber, column: monacoCursorPosition.column, engine, }); - if (!suggestions) { + if (!response) { return { items: [], }; } + store.dispatch(setSuggestions(response.items)); + store.dispatch(setRequestId(response.requestId)); + const range = getRangeToInsertSuggestion(model, monacoCursorPosition); return { - items: suggestions.map((item) => ({ - insertText: item, - range, - })), + items: response.items.map((item) => { + const data: AcceptedTelemetryData = { + requestId: response.requestId, + timestamp: Date.now(), + type: 'accepted', + acceptedText: item, + convertedText: item, + }; + + return { + insertText: item, + range, + command: { + id: 'neuralNetworkTelemetry', + title: 'string', + arguments: [data], + }, + }; + }), }; }; diff --git a/packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/api.ts b/packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/api.ts new file mode 100644 index 000000000..e54a2fac0 --- /dev/null +++ b/packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/api.ts @@ -0,0 +1,29 @@ +import {QueryEngine} from '../../../pages/query-tracker/module/engines'; +import axios from 'axios'; +import {NeuralNetworkTelemetryData} from '../../../../shared/neuralNetwork'; + +export type QuerySuggestionProps = { + contextId: string; + query: string; + line: number; + column: number; + engine: QueryEngine; +}; +const BASE_PATH = '/api/neural-network'; + +export const getNeuralNetworkSuggestions = async (data: QuerySuggestionProps) => { + const response = await axios.get<{items: string[]; requestId: string}>( + `${BASE_PATH}/query-suggestions`, + { + params: data, + }, + ); + return response.data; +}; + +export const sendNeuralNetworkTelemetry = async (data: NeuralNetworkTelemetryData) => { + const response = await axios.post(`${BASE_PATH}/send-telemetry`, { + telemetry: data, + }); + return response.data; +}; diff --git a/packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/useMonacoNeuralNetwork.ts b/packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/useMonacoNeuralNetwork.ts new file mode 100644 index 000000000..8b6cbf7f7 --- /dev/null +++ b/packages/ui/src/ui/libs/monaco-yql-languages/neuralNetwork/useMonacoNeuralNetwork.ts @@ -0,0 +1,46 @@ +import {useCallback, useEffect} from 'react'; +import * as monaco from 'monaco-editor'; +import { + selectNeuralNetworkRequestId, + selectNeuralNetworkSuggestions, +} from '../../../pages/query-tracker/module/neuralNetwork/selectors'; +import {sendNeuralNetworkTelemetry} from './api'; +import {useSelector} from 'react-redux'; +import {DiscardedTelemetryData} from '../../../../shared/neuralNetwork'; + +export const useMonacoNeuralNetwork = (editor?: monaco.editor.IStandaloneCodeEditor) => { + const requestId = useSelector(selectNeuralNetworkRequestId); + const suggestions = useSelector(selectNeuralNetworkSuggestions); + + const handleCancelTelemetry = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape' && editor) { + const data: DiscardedTelemetryData = { + requestId, + timestamp: Date.now(), + type: 'discarded', + reason: 'OnCancel', + discardedText: suggestions[0], + }; + editor.trigger(undefined, 'neuralNetworkTelemetry', data); + } + }, + [editor, requestId, suggestions], + ); + + useEffect(() => { + monaco.editor.registerCommand('neuralNetworkTelemetry', async (_accessor, ...args) => { + if (args.length > 0) { + await sendNeuralNetworkTelemetry(args[0]); + } + }); + }, []); + + useEffect(() => { + document.addEventListener('keydown', handleCancelTelemetry, true); + + return () => { + document.removeEventListener('keydown', handleCancelTelemetry, true); + }; + }, [handleCancelTelemetry, editor]); +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryEditor/QueryEditor.tsx b/packages/ui/src/ui/pages/query-tracker/QueryEditor/QueryEditor.tsx index e9675538a..576d70267 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryEditor/QueryEditor.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueryEditor/QueryEditor.tsx @@ -7,9 +7,9 @@ import {Button, Flex, Icon, Loader} from '@gravity-ui/uikit'; import playIcon from '../../../assets/img/svg/play.svg'; import {useDispatch, useSelector} from 'react-redux'; import { - getQuery, getQueryEditorErrors, getQueryEngine, + getQueryId, getQueryText, isQueryExecuted, isQueryLoading, @@ -41,6 +41,7 @@ import {WaitForFont} from '../../../containers/WaitForFont/WaitForFont'; import {getHashLineNumber} from './helpers/getHashLineNumber'; import {makeHighlightedLineDecorator} from './helpers/makeHighlightedLineDecorator'; import {getDecorationsWithoutHighlight} from './helpers/getDecorationsWithoutHighlight'; +import {useMonacoNeuralNetwork} from '../../../libs/monaco-yql-languages/neuralNetwork/useMonacoNeuralNetwork'; const b = block('query-container'); @@ -65,7 +66,7 @@ const QueryEditorView = React.memo(function QueryEditorView({ const [changed, setChanged] = useState(false); const editorRef = useRef(); const {setEditor} = useMonaco(); - const activeQuery = useSelector(getQuery); + const id = useSelector(getQueryId); const text = useSelector(getQueryText); const engine = useSelector(getQueryEngine); const editorErrors = useSelector(getQueryEditorErrors); @@ -76,6 +77,7 @@ const QueryEditorView = React.memo(function QueryEditorView({ undefined, ); const model = editorRef.current?.getModel(); + useMonacoNeuralNetwork(editorRef.current); const runQueryCallback = useCallback(() => { dispatch(runQuery(onStartQuery)); @@ -84,7 +86,7 @@ const QueryEditorView = React.memo(function QueryEditorView({ useEffect(() => { editorRef.current?.focus(); editorRef.current?.setScrollTop(0); - }, [activeQuery?.id]); + }, [id]); useEffect(() => { if (editorRef.current) {