mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
a6943cb399
commit
c412a3b052
@ -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…
Reference in New Issue
Block a user