From 3d785e8463f2232111aab091e42d025fac49d12d Mon Sep 17 00:00:00 2001 From: "JUST.in DO IT" Date: Fri, 14 Jun 2024 09:35:17 -0700 Subject: [PATCH] chore(sqllab): Add logging for actions (#28876) --- .../src/SqlLab/components/ResultSet/index.tsx | 19 ++++++- .../components/RunQueryActionButton/index.tsx | 14 ++++- .../src/SqlLab/components/SaveQuery/index.tsx | 14 ++++- .../components/ShareSqlLabQuery/index.tsx | 7 ++- .../src/SqlLab/components/SqlEditor/index.tsx | 52 ++++++++++++++++-- .../components/TabbedSqlEditors/index.tsx | 12 +++- superset-frontend/src/logger/LogUtils.ts | 16 ++++++ .../src/logger/useLogAction.test.ts | 55 +++++++++++++++++++ superset-frontend/src/logger/useLogAction.ts | 40 ++++++++++++++ 9 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 superset-frontend/src/logger/useLogAction.test.ts create mode 100644 superset-frontend/src/logger/useLogAction.ts diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 3496624aa64cf..ac41d19b82435 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -71,6 +71,12 @@ import { reRunQuery, } from 'src/SqlLab/actions/sqlLab'; import { URL_PARAMS } from 'src/constants'; +import useLogAction from 'src/logger/useLogAction'; +import { + LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, + LOG_ACTIONS_SQLLAB_CREATE_CHART, + LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV, +} from 'src/logger/LogUtils'; import Icons from 'src/components/Icons'; import ExploreCtasResultsButton from '../ExploreCtasResultsButton'; import ExploreResultsButton from '../ExploreResultsButton'; @@ -171,6 +177,7 @@ const ResultSet = ({ 'dbId', 'tab', 'sql', + 'sqlEditorId', 'templateParams', 'schema', 'rows', @@ -201,6 +208,7 @@ const ResultSet = ({ const history = useHistory(); const dispatch = useDispatch(); + const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId }); const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => { if ( @@ -258,7 +266,7 @@ const ResultSet = ({ const { results } = query; const openInNewWindow = clickEvent.metaKey; - + logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {}); if (results?.query_id) { const key = await postFormData(results.query_id, 'query', { ...EXPLORE_CHART_DEFAULT, @@ -321,7 +329,11 @@ const ResultSet = ({ /> )} {csv && ( - )} @@ -335,6 +347,9 @@ const ResultSet = ({ } hideTooltip + onCopyEnd={() => + logAction(LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, {}) + } /> {search && ( diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx index 40769c110c4f2..0992d64a79264 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx @@ -26,6 +26,11 @@ import { DropdownButton } from 'src/components/DropdownButton'; import { detectOS } from 'src/utils/common'; import { QueryButtonProps } from 'src/SqlLab/types'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { + LOG_ACTIONS_SQLLAB_RUN_QUERY, + LOG_ACTIONS_SQLLAB_STOP_QUERY, +} from 'src/logger/LogUtils'; +import useLogAction from 'src/logger/useLogAction'; export interface RunQueryActionButtonProps { queryEditorId: string; @@ -58,7 +63,13 @@ const onClick = ( allowAsync: boolean, runQuery: (c?: boolean) => void = () => undefined, stopQuery = () => {}, + logAction: (name: string, payload: Record) => void, ): void => { + const eventName = shouldShowStopButton + ? LOG_ACTIONS_SQLLAB_STOP_QUERY + : LOG_ACTIONS_SQLLAB_RUN_QUERY; + + logAction(eventName, { shortcut: false }); if (shouldShowStopButton) return stopQuery(); if (allowAsync) { return runQuery(true); @@ -90,6 +101,7 @@ const RunQueryActionButton = ({ stopQuery, }: RunQueryActionButtonProps) => { const theme = useTheme(); + const logAction = useLogAction({ queryEditorId }); const userOS = detectOS(); const { selectedText, sql } = useQueryEditor(queryEditorId, [ @@ -122,7 +134,7 @@ const RunQueryActionButton = ({ - onClick(shouldShowStopBtn, allowAsync, runQuery, stopQuery) + onClick(shouldShowStopBtn, allowAsync, runQuery, stopQuery, logAction) } disabled={isDisabled} tooltip={ diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx index 11b3d4233b437..dbebf6d9b4040 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx @@ -34,6 +34,11 @@ import { import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; import { QueryEditor } from 'src/SqlLab/types'; +import useLogAction from 'src/logger/useLogAction'; +import { + LOG_ACTIONS_SQLLAB_CREATE_CHART, + LOG_ACTIONS_SQLLAB_SAVE_QUERY, +} from 'src/logger/LogUtils'; interface SaveQueryProps { queryEditorId: string; @@ -92,6 +97,7 @@ const SaveQuery = ({ }), [queryEditor, columns], ); + const logAction = useLogAction({ queryEditorId }); const defaultLabel = query.name || query.description || t('Undefined'); const [description, setDescription] = useState( query.description || '', @@ -106,7 +112,12 @@ const SaveQuery = ({ const overlayMenu = ( - setShowSaveDatasetModal(true)}> + { + logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {}); + setShowSaveDatasetModal(true); + }} + > {t('Save dataset')} @@ -130,6 +141,7 @@ const SaveQuery = ({ const close = () => setShowSave(false); const onSaveWrapper = () => { + logAction(LOG_ACTIONS_SQLLAB_SAVE_QUERY, {}); onSave(queryPayload(), query.id); close(); }; diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx index a9a82c43f0162..c1a45eccadf21 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx @@ -30,6 +30,8 @@ import withToasts from 'src/components/MessageToasts/withToasts'; import CopyToClipboard from 'src/components/CopyToClipboard'; import { storeQuery } from 'src/utils/common'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { LOG_ACTIONS_SQLLAB_COPY_LINK } from 'src/logger/LogUtils'; +import useLogAction from 'src/logger/useLogAction'; interface ShareSqlLabQueryProps { queryEditorId: string; @@ -51,7 +53,7 @@ const ShareSqlLabQuery = ({ addDangerToast, }: ShareSqlLabQueryProps) => { const theme = useTheme(); - + const logAction = useLogAction({ queryEditorId }); const { dbId, name, schema, autorun, sql, remoteId, templateParams } = useQueryEditor(queryEditorId, [ 'dbId', @@ -91,6 +93,9 @@ const ShareSqlLabQuery = ({ } }; const getCopyUrl = (callback: Function) => { + logAction(LOG_ACTIONS_SQLLAB_COPY_LINK, { + shortcut: false, + }); if (isFeatureEnabled(FeatureFlag.ShareQueriesViaKvStore)) { return getCopyUrlForKvStore(callback); } diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index 1673220b379ad..1650968455b08 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx @@ -100,6 +100,17 @@ import { } from 'src/utils/localStorageHelpers'; import { EmptyStateBig } from 'src/components/EmptyState'; import getBootstrapData from 'src/utils/getBootstrapData'; +import useLogAction from 'src/logger/useLogAction'; +import { + LOG_ACTIONS_SQLLAB_CREATE_TABLE_AS, + LOG_ACTIONS_SQLLAB_CREATE_VIEW_AS, + LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST, + LOG_ACTIONS_SQLLAB_FORMAT_SQL, + LOG_ACTIONS_SQLLAB_LOAD_TAB_STATE, + LOG_ACTIONS_SQLLAB_RUN_QUERY, + LOG_ACTIONS_SQLLAB_STOP_QUERY, + Logger, +} from 'src/logger/LogUtils'; import TemplateParamsEditor from '../TemplateParamsEditor'; import SouthPane from '../SouthPane'; import SaveQuery, { QueryPayload } from '../SaveQuery'; @@ -273,6 +284,7 @@ const SqlEditor: FC = ({ }; }, shallowEqual); + const logAction = useLogAction({ queryEditorId: queryEditor.id }); const isActive = currentQueryEditorId === queryEditor.id; const [height, setHeight] = useState(0); const [autorun, setAutorun] = useState(queryEditor.autorun); @@ -319,9 +331,15 @@ const SqlEditor: FC = ({ [ctas, database, defaultQueryLimit, dispatch, queryEditor], ); - const formatCurrentQuery = useCallback(() => { - dispatch(formatQuery(queryEditor)); - }, [dispatch, queryEditor]); + const formatCurrentQuery = useCallback( + (useShortcut?: boolean) => { + logAction(LOG_ACTIONS_SQLLAB_FORMAT_SQL, { + shortcut: Boolean(useShortcut), + }); + dispatch(formatQuery(queryEditor)); + }, + [dispatch, queryEditor, logAction], + ); const stopQuery = useCallback(() => { if (latestQuery && ['running', 'pending'].indexOf(latestQuery.state) >= 0) { @@ -360,6 +378,7 @@ const SqlEditor: FC = ({ descr: KEY_MAP[KeyboardShortcut.CtrlR], func: () => { if (queryEditor.sql.trim() !== '') { + logAction(LOG_ACTIONS_SQLLAB_RUN_QUERY, { shortcut: true }); startQuery(); } }, @@ -370,6 +389,7 @@ const SqlEditor: FC = ({ descr: KEY_MAP[KeyboardShortcut.CtrlEnter], func: () => { if (queryEditor.sql.trim() !== '') { + logAction(LOG_ACTIONS_SQLLAB_RUN_QUERY, { shortcut: true }); startQuery(); } }, @@ -386,6 +406,7 @@ const SqlEditor: FC = ({ descr: KEY_MAP[KeyboardShortcut.CtrlT], }), func: () => { + Logger.markTimeOrigin(); dispatch(addNewQueryEditor()); }, }, @@ -400,14 +421,17 @@ const SqlEditor: FC = ({ key: KeyboardShortcut.CtrlE, descr: KEY_MAP[KeyboardShortcut.CtrlE], }), - func: stopQuery, + func: () => { + logAction(LOG_ACTIONS_SQLLAB_STOP_QUERY, { shortcut: true }); + stopQuery(); + }, }, { name: 'formatQuery', key: KeyboardShortcut.CtrlShiftF, descr: KEY_MAP[KeyboardShortcut.CtrlShiftF], func: () => { - formatCurrentQuery(); + formatCurrentQuery(true); }, }, ]; @@ -505,6 +529,13 @@ const SqlEditor: FC = ({ !queryEditor.loaded; const loadQueryEditor = useEffectEvent(() => { + const duration = Logger.getTimestamp(); + logAction(LOG_ACTIONS_SQLLAB_LOAD_TAB_STATE, { + duration, + queryEditorId: queryEditor.id, + inLocalStorage: Boolean(queryEditor.inLocalStorage), + hasLoaded: !shouldLoadQueryEditor, + }); if (shouldLoadQueryEditor) { dispatch(fetchQueryEditor(queryEditor, displayLimit)); } @@ -602,6 +633,7 @@ const SqlEditor: FC = ({ }); const getQueryCostEstimate = () => { + logAction(LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST, { shortcut: false }); if (database) { dispatch(estimateQueryCost(queryEditor)); } @@ -668,7 +700,9 @@ const SqlEditor: FC = ({ /> )} - {t('Format SQL')} + formatCurrentQuery()}> + {t('Format SQL')} + {!isEmpty(scheduledQueriesConf) && ( = ({ {allowCTAS && ( { + logAction(LOG_ACTIONS_SQLLAB_CREATE_TABLE_AS, { + shortcut: false, + }); setShowCreateAsModal(true); setCreateAs(CtasEnum.Table); }} @@ -717,6 +754,9 @@ const SqlEditor: FC = ({ {allowCVAS && ( { + logAction(LOG_ACTIONS_SQLLAB_CREATE_VIEW_AS, { + shortcut: false, + }); setShowCreateAsModal(true); setCreateAs(CtasEnum.View); }} diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx index d5ed291501496..df3099994ec31 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx @@ -23,6 +23,7 @@ import { connect } from 'react-redux'; import URI from 'urijs'; import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types'; import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core'; +import { Logger } from 'src/logger/LogUtils'; import { Tooltip } from 'src/components/Tooltip'; import { detectOS } from 'src/utils/common'; import * as Actions from 'src/SqlLab/actions/sqlLab'; @@ -193,6 +194,7 @@ class TabbedSqlEditors extends PureComponent { } } if (action === 'add') { + Logger.markTimeOrigin(); this.newQueryEditor(); } } @@ -201,6 +203,14 @@ class TabbedSqlEditors extends PureComponent { this.props.actions.removeQueryEditor(qe); } + onTabClicked = () => { + Logger.markTimeOrigin(); + const noQueryEditors = this.props.queryEditors?.length === 0; + if (noQueryEditors) { + this.newQueryEditor(); + } + }; + render() { const noQueryEditors = this.props.queryEditors?.length === 0; const editors = this.props.queryEditors?.map(qe => ( @@ -261,7 +271,7 @@ class TabbedSqlEditors extends PureComponent { onChange={this.handleSelect} fullWidth={false} hideAdd={this.props.offline} - onTabClick={() => noQueryEditors && this.newQueryEditor()} + onTabClick={this.onTabClicked} onEdit={this.handleEdit} type={noQueryEditors ? 'card' : 'editable-card'} addIcon={ diff --git a/superset-frontend/src/logger/LogUtils.ts b/superset-frontend/src/logger/LogUtils.ts index 2020cb67c0a92..913a3d5af7bb7 100644 --- a/superset-frontend/src/logger/LogUtils.ts +++ b/superset-frontend/src/logger/LogUtils.ts @@ -70,6 +70,21 @@ export const LOG_ACTIONS_DRILL_BY_BREADCRUMB_CLICKED = 'drill_by_breadcrumb_clicked'; export const LOG_ACTIONS_SQLLAB_MONITOR_LOCAL_STORAGE_USAGE = 'sqllab_monitor_local_storage_usage'; +export const LOG_ACTIONS_SQLLAB_CREATE_TABLE_AS = 'sqllab_create_table_as'; +export const LOG_ACTIONS_SQLLAB_CREATE_VIEW_AS = 'sqllab_create_view_as'; +export const LOG_ACTIONS_SQLLAB_RUN_QUERY = 'sqllab_run_query'; +export const LOG_ACTIONS_SQLLAB_STOP_QUERY = 'sqllab_stop_query'; +export const LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST = + 'sqllab_estimate_query_cost'; +export const LOG_ACTIONS_SQLLAB_SAVE_QUERY = 'sqllab_save_query'; +export const LOG_ACTIONS_SQLLAB_SAVE_DATASET = 'sqllab_save_dataset'; +export const LOG_ACTIONS_SQLLAB_COPY_LINK = 'sqllab_copy_link'; +export const LOG_ACTIONS_SQLLAB_FORMAT_SQL = 'sqllab_format_sql'; +export const LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV = 'sqllab_download_csv'; +export const LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD = + 'sqllab_copy_result_to_clipboard'; +export const LOG_ACTIONS_SQLLAB_CREATE_CHART = 'sqllab_create_chart'; +export const LOG_ACTIONS_SQLLAB_LOAD_TAB_STATE = 'sqllab_load_tab_state'; // Log event types -------------------------------------------------------------- export const LOG_EVENT_TYPE_TIMING = new Set([ @@ -77,6 +92,7 @@ export const LOG_EVENT_TYPE_TIMING = new Set([ LOG_ACTIONS_RENDER_CHART, LOG_ACTIONS_HIDE_BROWSER_TAB, LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY, + LOG_ACTIONS_SQLLAB_LOAD_TAB_STATE, ]); export const LOG_EVENT_TYPE_USER = new Set([ LOG_ACTIONS_MOUNT_DASHBOARD, diff --git a/superset-frontend/src/logger/useLogAction.test.ts b/superset-frontend/src/logger/useLogAction.test.ts new file mode 100644 index 0000000000000..bf314ff0ee9e7 --- /dev/null +++ b/superset-frontend/src/logger/useLogAction.test.ts @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { renderHook } from '@testing-library/react-hooks'; +import { createWrapper } from 'spec/helpers/testing-library'; +import useLogAction from './useLogAction'; +import { LOG_ACTIONS_SQLLAB_COPY_LINK } from './LogUtils'; +import { LOG_EVENT } from './actions'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +test('dispatches logEvent action with static EventData', () => { + const staticEventData = { staticEventKey: 'value1' }; + const store = mockStore(); + const { result } = renderHook(() => useLogAction(staticEventData), { + wrapper: createWrapper({ + useRedux: true, + store, + }), + }); + result.current(LOG_ACTIONS_SQLLAB_COPY_LINK, { count: 1 }); + store.getActions(); + expect(store.getActions()).toEqual([ + { + type: LOG_EVENT, + payload: { + eventName: LOG_ACTIONS_SQLLAB_COPY_LINK, + eventData: { + payload: { + ...staticEventData, + count: 1, + }, + }, + }, + }, + ]); +}); diff --git a/superset-frontend/src/logger/useLogAction.ts b/superset-frontend/src/logger/useLogAction.ts new file mode 100644 index 0000000000000..3f8cdcc50fccb --- /dev/null +++ b/superset-frontend/src/logger/useLogAction.ts @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { logEvent } from 'src/logger/actions'; + +export default function useLogAction(staticEventData: Record) { + const dispatch = useDispatch(); + const logAction = useCallback( + (type, payload) => + dispatch( + logEvent(type, { + payload: { + ...staticEventData, + ...payload, + }, + }), + ), + [staticEventData, dispatch], + ); + + return logAction; +}