SQL: Add feature tracking to sql datasources (#73996)

* add sql ds feature tracking

* feature tracking

* remove unused imports

* Update public/app/features/plugins/sql/components/QueryHeader.tsx

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>

* suggestion

* add datasource to report interaction

* report editor collapse

* pass ds on preview copy

* update missing instance checks

* add confirm modal data to report

---------

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>
This commit is contained in:
Gareth Dawson 2023-09-05 12:45:41 +01:00 committed by GitHub
parent c5a0d64652
commit f18cd13f2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 22 deletions

View File

@ -3,6 +3,7 @@ import { useCopyToClipboard } from 'react-use';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect, Space } from '@grafana/experimental'; import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect, Space } from '@grafana/experimental';
import { reportInteraction } from '@grafana/runtime';
import { Button, InlineSwitch, RadioButtonGroup, Tooltip } from '@grafana/ui'; import { Button, InlineSwitch, RadioButtonGroup, Tooltip } from '@grafana/ui';
import { QueryWithDefaults } from '../defaults'; import { QueryWithDefaults } from '../defaults';
@ -48,6 +49,13 @@ export function QueryHeader({
const onEditorModeChange = useCallback( const onEditorModeChange = useCallback(
(newEditorMode: EditorMode) => { (newEditorMode: EditorMode) => {
if (newEditorMode === EditorMode.Code) {
reportInteraction('grafana_sql_editor_mode_changed', {
datasource: query.datasource?.type,
selectedEditorMode: EditorMode.Code,
});
}
if (editorMode === EditorMode.Code) { if (editorMode === EditorMode.Code) {
setShowConfirm(true); setShowConfirm(true);
return; return;
@ -59,6 +67,11 @@ export function QueryHeader({
const onFormatChange = (e: SelectableValue) => { const onFormatChange = (e: SelectableValue) => {
const next = { ...query, format: e.value !== undefined ? e.value : QueryFormat.Table }; const next = { ...query, format: e.value !== undefined ? e.value : QueryFormat.Table };
reportInteraction('grafana_sql_format_changed', {
datasource: query.datasource?.type,
selectedFormat: next.format,
});
onChange(next); onChange(next);
}; };
@ -123,10 +136,18 @@ export function QueryHeader({
transparent={true} transparent={true}
showLabel={true} showLabel={true}
value={queryRowFilter.filter} value={queryRowFilter.filter}
onChange={(ev) => onChange={(ev) => {
ev.target instanceof HTMLInputElement && if (!(ev.target instanceof HTMLInputElement)) {
onQueryRowChange({ ...queryRowFilter, filter: ev.target.checked }) return;
} }
reportInteraction('grafana_sql_filter_toggled', {
datasource: query.datasource?.type,
displayed: ev.target.checked,
});
onQueryRowChange({ ...queryRowFilter, filter: ev.target.checked });
}}
/> />
<InlineSwitch <InlineSwitch
@ -135,10 +156,18 @@ export function QueryHeader({
transparent={true} transparent={true}
showLabel={true} showLabel={true}
value={queryRowFilter.group} value={queryRowFilter.group}
onChange={(ev) => onChange={(ev) => {
ev.target instanceof HTMLInputElement && if (!(ev.target instanceof HTMLInputElement)) {
onQueryRowChange({ ...queryRowFilter, group: ev.target.checked }) return;
} }
reportInteraction('grafana_sql_group_toggled', {
datasource: query.datasource?.type,
displayed: ev.target.checked,
});
onQueryRowChange({ ...queryRowFilter, group: ev.target.checked });
}}
/> />
<InlineSwitch <InlineSwitch
@ -147,10 +176,18 @@ export function QueryHeader({
transparent={true} transparent={true}
showLabel={true} showLabel={true}
value={queryRowFilter.order} value={queryRowFilter.order}
onChange={(ev) => onChange={(ev) => {
ev.target instanceof HTMLInputElement && if (!(ev.target instanceof HTMLInputElement)) {
onQueryRowChange({ ...queryRowFilter, order: ev.target.checked }) return;
} }
reportInteraction('grafana_sql_order_toggled', {
datasource: query.datasource?.type,
displayed: ev.target.checked,
});
onQueryRowChange({ ...queryRowFilter, order: ev.target.checked });
}}
/> />
<InlineSwitch <InlineSwitch
@ -159,10 +196,18 @@ export function QueryHeader({
transparent={true} transparent={true}
showLabel={true} showLabel={true}
value={queryRowFilter.preview} value={queryRowFilter.preview}
onChange={(ev) => onChange={(ev) => {
ev.target instanceof HTMLInputElement && if (!(ev.target instanceof HTMLInputElement)) {
onQueryRowChange({ ...queryRowFilter, preview: ev.target.checked }) return;
} }
reportInteraction('grafana_sql_preview_toggled', {
datasource: query.datasource?.type,
displayed: ev.target.checked,
});
onQueryRowChange({ ...queryRowFilter, preview: ev.target.checked });
}}
/> />
</> </>
)} )}
@ -195,6 +240,12 @@ export function QueryHeader({
<ConfirmModal <ConfirmModal
isOpen={showConfirm} isOpen={showConfirm}
onCopy={() => { onCopy={() => {
reportInteraction('grafana_sql_editor_mode_changed', {
datasource: query.datasource?.type,
selectedEditorMode: EditorMode.Builder,
type: 'copy',
});
setShowConfirm(false); setShowConfirm(false);
copyToClipboard(query.rawSql!); copyToClipboard(query.rawSql!);
onChange({ onChange({
@ -204,6 +255,12 @@ export function QueryHeader({
}); });
}} }}
onDiscard={() => { onDiscard={() => {
reportInteraction('grafana_sql_editor_mode_changed', {
datasource: query.datasource?.type,
selectedEditorMode: EditorMode.Builder,
type: 'discard',
});
setShowConfirm(false); setShowConfirm(false);
onChange({ onChange({
...query, ...query,
@ -211,7 +268,15 @@ export function QueryHeader({
editorMode: EditorMode.Builder, editorMode: EditorMode.Builder,
}); });
}} }}
onCancel={() => setShowConfirm(false)} onCancel={() => {
reportInteraction('grafana_sql_editor_mode_changed', {
datasource: query.datasource?.type,
selectedEditorMode: EditorMode.Builder,
type: 'cancel',
});
setShowConfirm(false);
}}
/> />
</EditorHeader> </EditorHeader>

View File

@ -1,6 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { reportInteraction } from '@grafana/runtime';
import { HorizontalGroup, Icon, IconButton, Tooltip, useTheme2 } from '@grafana/ui'; import { HorizontalGroup, Icon, IconButton, Tooltip, useTheme2 } from '@grafana/ui';
import { QueryValidator, QueryValidatorProps } from './QueryValidator'; import { QueryValidator, QueryValidatorProps } from './QueryValidator';
@ -70,11 +71,28 @@ export function QueryToolbox({ showTools, onFormatCode, onExpand, isExpanded, ..
<div> <div>
<HorizontalGroup spacing="sm"> <HorizontalGroup spacing="sm">
{onFormatCode && ( {onFormatCode && (
<IconButton onClick={onFormatCode} name="brackets-curly" size="xs" tooltip="Format query" /> <IconButton
onClick={() => {
reportInteraction('grafana_sql_query_formatted', {
datasource: validatorProps.query.datasource?.type,
});
onFormatCode();
}}
name="brackets-curly"
size="xs"
tooltip="Format query"
/>
)} )}
{onExpand && ( {onExpand && (
<IconButton <IconButton
onClick={() => onExpand(!isExpanded)} onClick={() => {
reportInteraction('grafana_sql_editor_expand', {
datasource: validatorProps.query.datasource?.type,
expanded: !isExpanded,
});
onExpand(!isExpanded);
}}
name={isExpanded ? 'angle-up' : 'angle-down'} name={isExpanded ? 'angle-up' : 'angle-down'}
size="xs" size="xs"
tooltip={isExpanded ? 'Collapse editor' : 'Expand editor'} tooltip={isExpanded ? 'Collapse editor' : 'Expand editor'}

View File

@ -4,6 +4,7 @@ import { useMeasure } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Modal, useStyles2, useTheme2 } from '@grafana/ui'; import { Modal, useStyles2, useTheme2 } from '@grafana/ui';
import { SQLQuery, QueryEditorProps } from '../../types'; import { SQLQuery, QueryEditorProps } from '../../types';
@ -97,6 +98,10 @@ export function RawEditor({ db, query, onChange, onRunQuery, onValidate, queryTo
contentClassName={styles.modalContent} contentClassName={styles.modalContent}
isOpen={isExpanded} isOpen={isExpanded}
onDismiss={() => { onDismiss={() => {
reportInteraction('grafana_sql_editor_expand', {
datasource: query.datasource?.type,
expanded: false,
});
setIsExpanded(false); setIsExpanded(false);
}} }}
> >

View File

@ -3,23 +3,32 @@ import React from 'react';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { CodeEditor, Field, IconButton, useStyles2 } from '@grafana/ui'; import { CodeEditor, Field, IconButton, useStyles2 } from '@grafana/ui';
import { formatSQL } from '../../utils/formatSQL'; import { formatSQL } from '../../utils/formatSQL';
type PreviewProps = { type PreviewProps = {
rawSql: string; rawSql: string;
datasourceType?: string;
}; };
export function Preview({ rawSql }: PreviewProps) { export function Preview({ rawSql, datasourceType }: PreviewProps) {
// TODO: use zero index to give feedback about copy success // TODO: use zero index to give feedback about copy success
const [_, copyToClipboard] = useCopyToClipboard(); const [_, copyToClipboard] = useCopyToClipboard();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const copyPreview = (rawSql: string) => {
copyToClipboard(rawSql);
reportInteraction('grafana_sql_preview_copied', {
datasource: datasourceType,
});
};
const labelElement = ( const labelElement = (
<div className={styles.labelWrapper}> <div className={styles.labelWrapper}>
<span className={styles.label}>Preview</span> <span className={styles.label}>Preview</span>
<IconButton tooltip="Copy to clipboard" onClick={() => copyToClipboard(rawSql)} name="copy" /> <IconButton tooltip="Copy to clipboard" onClick={() => copyPreview(rawSql)} name="copy" />
</div> </div>
); );

View File

@ -51,7 +51,7 @@ export const VisualEditor = ({ query, db, queryRowFilter, onChange, onValidate,
)} )}
{queryRowFilter.preview && query.rawSql && ( {queryRowFilter.preview && query.rawSql && (
<EditorRow> <EditorRow>
<Preview rawSql={query.rawSql} /> <Preview rawSql={query.rawSql} datasourceType={query.datasource?.type} />
</EditorRow> </EditorRow>
)} )}
</EditorRows> </EditorRows>

View File

@ -23,6 +23,7 @@ import {
getBackendSrv, getBackendSrv,
getTemplateSrv, getTemplateSrv,
TemplateSrv, TemplateSrv,
reportInteraction,
} from '@grafana/runtime'; } from '@grafana/runtime';
import { toDataQueryResponse } from '@grafana/runtime/src/utils/queryResponse'; import { toDataQueryResponse } from '@grafana/runtime/src/utils/queryResponse';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -138,6 +139,15 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
} }
} }
request.targets.forEach((target) => {
reportInteraction('grafana_sql_query_executed', {
datasource: target.datasource?.type,
editorMode: target.editorMode,
format: target.format,
app: request.app,
});
});
return super.query(request); return super.query(request);
} }