diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 87ba0370edafb..830ed3b9c2ab1 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -62,6 +62,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';
@@ -162,6 +168,7 @@ const ResultSet = ({
'dbId',
'tab',
'sql',
+ 'sqlEditorId',
'templateParams',
'schema',
'rows',
@@ -192,6 +199,7 @@ const ResultSet = ({
const history = useHistory();
const dispatch = useDispatch();
+ const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => {
if (
@@ -249,7 +257,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,
@@ -312,7 +320,11 @@ const ResultSet = ({
/>
)}
{csv && (
-
}
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 30f8121132162..0d1740cbac634 100644
--- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
@@ -25,6 +25,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;
@@ -57,7 +62,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);
@@ -89,6 +100,7 @@ const RunQueryActionButton = ({
stopQuery,
}: RunQueryActionButtonProps) => {
const theme = useTheme();
+ const logAction = useLogAction({ queryEditorId });
const userOS = detectOS();
const { selectedText, sql } = useQueryEditor(queryEditorId, [
@@ -121,7 +133,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 a7ac8b1b2a896..766c37cfbd778 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -33,6 +33,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;
@@ -90,6 +95,7 @@ const SaveQuery = ({
}),
[queryEditor, columns],
);
+ const logAction = useLogAction({ queryEditorId });
const defaultLabel = query.name || query.description || t('Undefined');
const [description, setDescription] = useState(
query.description || '',
@@ -104,7 +110,12 @@ const SaveQuery = ({
const overlayMenu = (
@@ -127,6 +138,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 8f4cde1a66f17..b0a63f56e0ffc 100644
--- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx
@@ -31,6 +31,8 @@ import CopyToClipboard from 'src/components/CopyToClipboard';
import { storeQuery } from 'src/utils/common';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
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;
@@ -52,7 +54,7 @@ const ShareSqlLabQuery = ({
addDangerToast,
}: ShareSqlLabQueryProps) => {
const theme = useTheme();
-
+ const logAction = useLogAction({ queryEditorId });
const { dbId, name, schema, autorun, sql, remoteId, templateParams } =
useQueryEditor(queryEditorId, [
'dbId',
@@ -92,6 +94,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 15907269531d3..0bf256b3a33c0 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -98,6 +98,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';
@@ -271,6 +282,7 @@ const SqlEditor: React.FC = ({
};
}, shallowEqual);
+ const logAction = useLogAction({ queryEditorId: queryEditor.id });
const isActive = currentQueryEditorId === queryEditor.id;
const [height, setHeight] = useState(0);
const [autorun, setAutorun] = useState(queryEditor.autorun);
@@ -314,9 +326,15 @@ const SqlEditor: React.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) {
@@ -355,6 +373,7 @@ const SqlEditor: React.FC = ({
descr: KEY_MAP[KeyboardShortcut.CtrlR],
func: () => {
if (queryEditor.sql.trim() !== '') {
+ logAction(LOG_ACTIONS_SQLLAB_RUN_QUERY, { shortcut: true });
startQuery();
}
},
@@ -365,6 +384,7 @@ const SqlEditor: React.FC = ({
descr: KEY_MAP[KeyboardShortcut.CtrlEnter],
func: () => {
if (queryEditor.sql.trim() !== '') {
+ logAction(LOG_ACTIONS_SQLLAB_RUN_QUERY, { shortcut: true });
startQuery();
}
},
@@ -381,6 +401,7 @@ const SqlEditor: React.FC = ({
descr: KEY_MAP[KeyboardShortcut.CtrlT],
}),
func: () => {
+ Logger.markTimeOrigin();
dispatch(addNewQueryEditor());
},
},
@@ -395,14 +416,17 @@ const SqlEditor: React.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);
},
},
];
@@ -500,6 +524,13 @@ const SqlEditor: React.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(switchQueryEditor(queryEditor, displayLimit));
}
@@ -597,6 +628,7 @@ const SqlEditor: React.FC = ({
});
const getQueryCostEstimate = () => {
+ logAction(LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST, { shortcut: false });
if (database) {
dispatch(estimateQueryCost(queryEditor));
}
@@ -650,7 +682,9 @@ const SqlEditor: React.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);
}}
@@ -699,6 +736,9 @@ const SqlEditor: React.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 078276ad26abf..c9532fc2f21be 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';
@@ -220,6 +221,7 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
if (action === 'add') {
+ Logger.markTimeOrigin();
this.newQueryEditor();
}
}
@@ -228,6 +230,14 @@ class TabbedSqlEditors extends React.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 => (
@@ -288,7 +298,7 @@ class TabbedSqlEditors extends React.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;
+}