logs: track the usage of certain features (#50325)

* logs: track the usage of certain features

* Add report interaction for logs interactions

* mock reportInteraction in test

* mock reportInteraction

Co-authored-by: Ivana Huckova <ivana.huckova@gmail.com>
pull/50525/head
Gábor Farkas 3 years ago committed by GitHub
parent a6943cb399
commit c412a3b052
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      public/app/features/dashboard/components/Inspector/InspectContent.tsx
  2. 3
      public/app/features/explore/ExploreQueryInspector.tsx
  3. 36
      public/app/features/explore/ExploreToolbar.tsx
  4. 6
      public/app/features/explore/Logs.tsx
  5. 1
      public/app/features/explore/LogsContainer.tsx
  6. 6
      public/app/features/explore/LogsNavigation.test.tsx
  7. 7
      public/app/features/explore/LogsNavigation.tsx
  8. 9
      public/app/features/explore/LogsNavigationPages.tsx
  9. 1
      public/app/features/explore/spec/queryHistory.test.tsx
  10. 7
      public/app/features/explore/state/query.ts
  11. 21
      public/app/features/inspector/InspectDataTab.tsx
  12. 8
      public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
  13. 6
      public/app/plugins/datasource/loki/components/LokiLabelBrowser.test.tsx
  14. 16
      public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx
  15. 13
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  16. 10
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx
  17. 1
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx
  18. 2
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
import { CoreApp, DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { Drawer, Tab, TabsBar } from '@grafana/ui';
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
@ -90,6 +90,7 @@ export const InspectContent: React.FC<Props> = ({
options={dataOptions}
onOptionsChange={onDataOptionsChange}
timeZone={dashboard.timezone}
app={CoreApp.Dashboard}
/>
)}
{data && activeTab === InspectTab.Meta && (

@ -1,7 +1,7 @@
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { TimeZone } from '@grafana/data';
import { CoreApp, TimeZone } from '@grafana/data';
import { TabbedContainer, TabConfig } from '@grafana/ui';
import { ExploreDrawer } from 'app/features/explore/ExploreDrawer';
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
@ -51,6 +51,7 @@ export function ExploreQueryInspector(props: Props) {
isLoading={loading}
options={{ withTransforms: false, withFieldConfig: false }}
timeZone={timeZone}
app={CoreApp.Explore}
/>
),
};

@ -2,7 +2,7 @@ import React, { lazy, PureComponent, RefObject, Suspense } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, RawTimeRange } from '@grafana/data';
import { config, DataSourcePicker } from '@grafana/runtime';
import { config, DataSourcePicker, reportInteraction } from '@grafana/runtime';
import {
defaultIntervals,
PageToolbar,
@ -192,17 +192,28 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
{(c) => {
const controls = {
...c,
start: () => {
reportInteraction('grafana_explore_logs_result_displayed', {
datasourceType: this.props.datasourceType,
});
c.start();
},
};
return (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
);
}}
</LiveTailControls>
)}
</ToolbarButtonRow>
@ -223,6 +234,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
return {
datasourceMissing,
datasourceName: datasourceInstance?.name,
datasourceType: datasourceInstance?.type,
loading,
range,
timeZone: getTimeZone(state.user),

@ -23,6 +23,7 @@ import {
SplitOpen,
DataQueryResponse,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import {
RadioButtonGroup,
@ -68,6 +69,7 @@ interface Props extends Themeable2 {
scanning?: boolean;
scanRange?: RawTimeRange;
exploreId: ExploreId;
datasourceType?: string;
logsVolumeData: DataQueryResponse | undefined;
loadLogsVolumeData: (exploreId: ExploreId) => void;
showContextToggle?: (row?: LogRowModel) => boolean;
@ -144,6 +146,10 @@ class UnthemedLogs extends PureComponent<Props, State> {
};
onChangeDedup = (dedupStrategy: LogsDedupStrategy) => {
reportInteraction('grafana_explore_logs_deduplication_clicked', {
deduplicationType: dedupStrategy,
datasourceType: this.props.datasourceType,
});
this.setState({ dedupStrategy });
};

@ -131,6 +131,7 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
<Collapse label="Logs" loading={loading} isOpen className={styleOverridesForStickyNavigation}>
<Logs
exploreId={exploreId}
datasourceType={this.props.datasourceInstance?.type}
logRows={logRows}
logsMeta={logsMeta}
logsSeries={logsSeries}

@ -5,6 +5,12 @@ import { LogsSortOrder } from '@grafana/data';
import LogsNavigation from './LogsNavigation';
// we have to mock out reportInteraction, otherwise it crashes the test.
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
reportInteraction: () => null,
}));
type LogsNavigationProps = ComponentProps<typeof LogsNavigation>;
const defaultProps: LogsNavigationProps = {
absoluteRange: { from: 1637319381811, to: 1637322981811 },

@ -3,6 +3,7 @@ import { isEqual } from 'lodash';
import React, { memo, useState, useEffect, useRef } from 'react';
import { LogsSortOrder, AbsoluteTimeRange, TimeZone, DataQuery, GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui';
import { LogsNavigationPages } from './LogsNavigationPages';
@ -107,6 +108,9 @@ function LogsNavigation({
variant="secondary"
onClick={() => {
//If we are not on the last page, use next page's range
reportInteraction('grafana_explore_logs_pagination_clicked', {
pageType: 'olderLogsButton',
});
if (!onLastPage) {
const indexChange = oldestLogsFirst ? -1 : 1;
changeTime({
@ -133,6 +137,9 @@ function LogsNavigation({
className={styles.navButton}
variant="secondary"
onClick={() => {
reportInteraction('grafana_explore_logs_pagination_clicked', {
pageType: 'newerLogsButton',
});
//If we are not on the first page, use previous page's range
if (!onFirstPage) {
const indexChange = oldestLogsFirst ? 1 : -1;

@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
import React from 'react';
import { dateTimeFormat, systemDateFormats, TimeZone, AbsoluteTimeRange, GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { CustomScrollbar, Spinner, useTheme2 } from '@grafana/ui';
import { LogsPage } from './LogsNavigation';
@ -51,7 +52,13 @@ export function LogsNavigationPages({
data-testid={`page${index + 1}`}
className={styles.page}
key={page.queryRange.to}
onClick={() => !loading && changeTime({ from: page.queryRange.from, to: page.queryRange.to })}
onClick={() => {
reportInteraction('grafana_explore_logs_pagination_clicked', {
pageType: 'page',
pageNumber: index + 1,
});
!loading && changeTime({ from: page.queryRange.from, to: page.queryRange.to });
}}
>
<div className={cx(styles.line, { selectedBg: currentPageIndex === index })} />
<div className={cx(styles.time, { selectedText: currentPageIndex === index })}>

@ -118,7 +118,6 @@ describe('Explore: Query History', () => {
await openQueryHistory();
await assertQueryHistoryExists(RAW_QUERY);
expect(reportInteractionMock).toBeCalledTimes(2);
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
queryHistoryEnabled: false,
});

@ -19,7 +19,7 @@ import {
QueryFixAction,
toLegacyResponseData,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { config, reportInteraction } from '@grafana/runtime';
import {
buildQueryTransaction,
ensureQueries,
@ -442,6 +442,11 @@ export const runQueries = (
)
.subscribe({
next(data) {
if (data.logsResult !== null) {
reportInteraction('grafana_explore_logs_result_displayed', {
datasourceType: datasourceInstance.type,
});
}
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
// Keep scanning for results if this was the last scanning transaction

@ -16,8 +16,10 @@ import {
toCSV,
transformDataFrame,
TimeZone,
CoreApp,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Button, Spinner, Table } from '@grafana/ui';
import { config } from 'app/core/config';
import { dataFrameToLogsModel } from 'app/core/logs_model';
@ -34,6 +36,7 @@ interface Props {
isLoading: boolean;
options: GetDataOptions;
timeZone: TimeZone;
app?: CoreApp;
data?: DataFrame[];
panel?: PanelModel;
onOptionsChange?: (options: GetDataOptions) => void;
@ -107,7 +110,11 @@ export class InspectDataTab extends PureComponent<Props, State> {
};
exportLogsAsTxt = () => {
const { data, panel } = this.props;
const { data, panel, app } = this.props;
reportInteraction('grafana_logs_download_logs_clicked', {
app,
format: 'logs',
});
const logsModel = dataFrameToLogsModel(data || [], undefined);
let textToDownload = '';
@ -223,7 +230,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
}
render() {
const { isLoading, options, data, panel, onOptionsChange } = this.props;
const { isLoading, options, data, panel, onOptionsChange, app } = this.props;
const { dataFrameIndex, transformId, transformationOptions, selectedDataFrame, downloadForExcel } = this.state;
const styles = getPanelInspectorStyles();
@ -266,7 +273,15 @@ export class InspectDataTab extends PureComponent<Props, State> {
/>
<Button
variant="primary"
onClick={() => this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel })}
onClick={() => {
if (hasLogs) {
reportInteraction('grafana_logs_download_clicked', {
app,
format: 'csv',
});
}
this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel });
}}
className={css`
margin-bottom: 10px;
`}

@ -2,6 +2,7 @@ import { shuffle } from 'lodash';
import React, { PureComponent } from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import LokiLanguageProvider from '../language_provider';
import { LokiQuery } from '../types';
@ -43,6 +44,7 @@ export default class LokiCheatSheet extends PureComponent<QueryEditorHelpProps<L
componentDidMount() {
this.scheduleUserLabelChecking();
reportInteraction('grafana_loki_cheatsheet_opened', {});
}
componentWillUnmount() {
@ -73,9 +75,13 @@ export default class LokiCheatSheet extends PureComponent<QueryEditorHelpProps<L
renderExpression(expr: string) {
const { onClickExample } = this.props;
const onClick = (query: LokiQuery) => {
onClickExample(query);
reportInteraction('grafana_loki_cheatsheet_example_clicked', {});
};
return (
<div className="cheat-sheet-item__example" key={expr} onClick={(e) => onClickExample({ refId: 'A', expr })}>
<div className="cheat-sheet-item__example" key={expr} onClick={(e) => onClick({ refId: 'A', expr })}>
<code>{expr}</code>
</div>
);

@ -14,6 +14,12 @@ import {
BrowserProps,
} from './LokiLabelBrowser';
// we have to mock out reportInteraction, otherwise it crashes the test.
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
reportInteraction: () => null,
}));
describe('buildSelector()', () => {
it('returns an empty selector for no labels', () => {
expect(buildSelector([])).toEqual('{}');

@ -3,7 +3,8 @@ import { sortBy } from 'lodash';
import React, { ChangeEvent } from 'react';
import { FixedSizeList } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data';
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import {
Button,
HighlightPart,
@ -31,6 +32,7 @@ export interface BrowserProps {
languageProvider: LokiLanguageProvider | PromQlLanguageProvider;
onChange: (selector: string) => void;
theme: GrafanaTheme2;
app?: CoreApp;
autoSelect?: number;
hide?: () => void;
lastUsedLabels: string[];
@ -189,17 +191,29 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
};
onClickRunLogsQuery = () => {
reportInteraction('grafana_loki_log_browser_closed', {
app: this.props.app,
closeType: 'showLogsButton',
});
const selector = buildSelector(this.state.labels);
this.props.onChange(selector);
};
onClickRunMetricsQuery = () => {
reportInteraction('grafana_loki_log_browser_closed', {
app: this.props.app,
closeType: 'showLogsRateButton',
});
const selector = buildSelector(this.state.labels);
const query = `rate(${selector}[$__interval])`;
this.props.onChange(query);
};
onClickClear = () => {
reportInteraction('grafana_loki_log_browser_closed', {
app: this.props.app,
closeType: 'clearButton',
});
this.setState((state) => {
const labels: SelectableLabel[] = state.labels.map((label) => ({
...label,

@ -3,6 +3,7 @@ import React, { ReactNode } from 'react';
import { Plugin, Node } from 'slate';
import { QueryEditorProps } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import {
SlatePrism,
TypeaheadOutput,
@ -145,6 +146,16 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
};
onClickChooserButton = () => {
if (!this.state.labelBrowserVisible) {
reportInteraction('grafana_loki_log_browser_opened', {
app: this.props.app,
});
} else {
reportInteraction('grafana_loki_log_browser_closed', {
app: this.props.app,
closeType: 'logBrowserButton',
});
}
this.setState((state) => ({ labelBrowserVisible: !state.labelBrowserVisible }));
};
@ -170,6 +181,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
const {
ExtraFieldElement,
query,
app,
datasource,
placeholder = 'Enter a Loki query (run with Shift+Enter)',
} = this.props;
@ -221,6 +233,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
lastUsedLabels={lastUsedLabels || []}
storeLastUsedLabels={onLastUsedLabelsSave}
deleteLastUsedLabels={onLastUsedLabelsDelete}
app={app}
/>
</div>
)}

@ -1,7 +1,8 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { CoreApp, SelectableValue } from '@grafana/data';
import { EditorRow, EditorField } from '@grafana/experimental';
import { reportInteraction } from '@grafana/runtime';
import { RadioButtonGroup, Select, AutoSizeInput } from '@grafana/ui';
import { QueryOptionGroup } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup';
@ -13,15 +14,20 @@ export interface Props {
query: LokiQuery;
onChange: (update: LokiQuery) => void;
onRunQuery: () => void;
app?: CoreApp;
}
export const LokiQueryBuilderOptions = React.memo<Props>(({ query, onChange, onRunQuery }) => {
export const LokiQueryBuilderOptions = React.memo<Props>(({ app, query, onChange, onRunQuery }) => {
const onQueryTypeChange = (value: LokiQueryType) => {
onChange({ ...query, queryType: value });
onRunQuery();
};
const onResolutionChange = (option: SelectableValue<number>) => {
reportInteraction('grafana_loki_resolution_clicked', {
app,
resolution: option.value,
});
onChange({ ...query, resolution: option.value });
onRunQuery();
};

@ -40,6 +40,7 @@ export function LokiQueryCodeEditor({
history={[]}
data={data}
data-testid={testIds.editor}
app={app}
/>
</div>
);

@ -126,7 +126,7 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
)}
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={query.expr} />}
{editorMode !== QueryEditorMode.Explain && (
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} />
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} app={app} />
)}
</EditorRows>
</>

Loading…
Cancel
Save