New Logs Panel: Add events for Panel and Details (#108272)

* New Logs Panel: Add events for Panel and Details

* Logs Panel: add noInteractions property

* New Logs Panel: conditionally report interactions

* Test
instant
Matias Chomicki 3 days ago committed by GitHub
parent f7a1398cd4
commit 046134db22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts
  2. 27
      public/app/features/logs/components/panel/LogLineDetails.tsx
  3. 11
      public/app/features/logs/components/panel/LogLineDetailsComponent.tsx
  4. 37
      public/app/features/logs/components/panel/LogLineDetailsFields.tsx
  5. 37
      public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
  6. 18
      public/app/features/logs/components/panel/LogList.test.tsx
  7. 3
      public/app/features/logs/components/panel/LogList.tsx
  8. 34
      public/app/features/logs/components/panel/LogListContext.tsx
  9. 23
      public/app/features/logs/components/panel/LogListSearch.tsx
  10. 2
      public/app/plugins/panel/logs/LogsPanel.tsx
  11. 1
      public/app/plugins/panel/logs/panelcfg.cue
  12. 1
      public/app/plugins/panel/logs/panelcfg.gen.ts

@ -24,6 +24,7 @@ export interface Options {
logLineMenuCustomItems?: unknown; logLineMenuCustomItems?: unknown;
logRowMenuIconsAfter?: unknown; logRowMenuIconsAfter?: unknown;
logRowMenuIconsBefore?: unknown; logRowMenuIconsBefore?: unknown;
noInteractions?: boolean;
/** /**
* TODO: figure out how to define callbacks * TODO: figure out how to define callbacks
*/ */

@ -3,6 +3,7 @@ import { Resizable } from 're-resizable';
import { memo, useCallback, useEffect, useRef } from 'react'; import { memo, useCallback, useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { getDragStyles, useStyles2 } from '@grafana/ui'; import { getDragStyles, useStyles2 } from '@grafana/ui';
import { LogLineDetailsComponent } from './LogLineDetailsComponent'; import { LogLineDetailsComponent } from './LogLineDetailsComponent';
@ -20,13 +21,18 @@ export interface Props {
export type LogLineDetailsMode = 'inline' | 'sidebar'; export type LogLineDetailsMode = 'inline' | 'sidebar';
export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize }: Props) => { export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize }: Props) => {
const { detailsWidth, setDetailsWidth, showDetails } = useLogListContext(); const { detailsWidth, noInteractions, setDetailsWidth, showDetails } = useLogListContext();
const styles = useStyles2(getStyles, 'sidebar'); const styles = useStyles2(getStyles, 'sidebar');
const dragStyles = useStyles2(getDragStyles); const dragStyles = useStyles2(getDragStyles);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
focusLogLine(showDetails[0]); focusLogLine(showDetails[0]);
if (!noInteractions) {
reportInteraction('logs_log_line_details_displayed', {
mode: 'sidebar',
});
}
// Just once // Just once
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@ -38,6 +44,14 @@ export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize
onResize(); onResize();
}, [onResize, setDetailsWidth]); }, [onResize, setDetailsWidth]);
const reportResize = useCallback(() => {
if (containerRef.current && !noInteractions) {
reportInteraction('logs_log_line_details_sidebar_resized', {
width: Math.round(containerRef.current.clientWidth),
});
}
}, [noInteractions]);
const maxWidth = containerElement.clientWidth - LOG_LIST_MIN_WIDTH; const maxWidth = containerElement.clientWidth - LOG_LIST_MIN_WIDTH;
if (!showDetails.length) { if (!showDetails.length) {
@ -47,6 +61,7 @@ export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize
return ( return (
<Resizable <Resizable
onResize={handleResize} onResize={handleResize}
onResizeStop={reportResize}
handleClasses={{ left: dragStyles.dragHandleVertical }} handleClasses={{ left: dragStyles.dragHandleVertical }}
defaultSize={{ width: detailsWidth, height: containerElement.clientHeight }} defaultSize={{ width: detailsWidth, height: containerElement.clientHeight }}
size={{ width: detailsWidth, height: containerElement.clientHeight }} size={{ width: detailsWidth, height: containerElement.clientHeight }}
@ -68,10 +83,18 @@ export interface InlineLogLineDetailsProps {
} }
export const InlineLogLineDetails = memo(({ logs }: InlineLogLineDetailsProps) => { export const InlineLogLineDetails = memo(({ logs }: InlineLogLineDetailsProps) => {
const { showDetails } = useLogListContext(); const { noInteractions, showDetails } = useLogListContext();
const styles = useStyles2(getStyles, 'inline'); const styles = useStyles2(getStyles, 'inline');
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!noInteractions) {
reportInteraction('logs_log_line_details_displayed', {
mode: 'inline',
});
}
}, [noInteractions]);
const saveScroll = useCallback(() => { const saveScroll = useCallback(() => {
saveDetailsScrollPosition(showDetails[0], scrollRef.current?.scrollTop ?? 0); saveDetailsScrollPosition(showDetails[0], scrollRef.current?.scrollTop ?? 0);
}, [showDetails]); }, [showDetails]);

@ -4,6 +4,7 @@ import { memo, startTransition, useCallback, useMemo, useRef, useState } from 'r
import { DataFrameType, GrafanaTheme2, store } from '@grafana/data'; import { DataFrameType, GrafanaTheme2, store } from '@grafana/data';
import { t, Trans } from '@grafana/i18n'; import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { ControlledCollapse, useStyles2 } from '@grafana/ui'; import { ControlledCollapse, useStyles2 } from '@grafana/ui';
import { getLabelTypeFromRow } from '../../utils'; import { getLabelTypeFromRow } from '../../utils';
@ -23,7 +24,7 @@ interface LogLineDetailsComponentProps {
} }
export const LogLineDetailsComponent = memo(({ log, logs }: LogLineDetailsComponentProps) => { export const LogLineDetailsComponent = memo(({ log, logs }: LogLineDetailsComponentProps) => {
const { displayedFields, logOptionsStorageKey, setDisplayedFields } = useLogListContext(); const { displayedFields, noInteractions, logOptionsStorageKey, setDisplayedFields } = useLogListContext();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const inputRef = useRef(''); const inputRef = useRef('');
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -75,8 +76,14 @@ export const LogLineDetailsComponent = memo(({ log, logs }: LogLineDetailsCompon
const handleToggle = useCallback( const handleToggle = useCallback(
(option: string, isOpen: boolean) => { (option: string, isOpen: boolean) => {
store.set(`${logOptionsStorageKey}.log-details.${option}`, isOpen); store.set(`${logOptionsStorageKey}.log-details.${option}`, isOpen);
if (!noInteractions) {
reportInteraction('logs_log_line_details_section_toggled', {
section: option.replace('Open', ''),
state: isOpen ? 'open' : 'closed',
});
}
}, },
[logOptionsStorageKey] [logOptionsStorageKey, noInteractions]
); );
const handleSearch = useCallback((newSearch: string) => { const handleSearch = useCallback((newSearch: string) => {

@ -143,6 +143,7 @@ export const LogLineDetailsField = ({
closeDetails, closeDetails,
displayedFields, displayedFields,
isLabelFilterActive, isLabelFilterActive,
noInteractions,
onClickFilterLabel, onClickFilterLabel,
onClickFilterOutLabel, onClickFilterOutLabel,
onClickShowField, onClickShowField,
@ -179,53 +180,59 @@ export const LogLineDetailsField = ({
} }
}, [showFieldsStats, updateStats]); }, [showFieldsStats, updateStats]);
const reportInteractionWrapper = useCallback(
(interactionName: string, properties?: Record<string, unknown>) => {
if (noInteractions) {
return;
}
reportInteraction(interactionName, properties);
},
[noInteractions]
);
const showField = useCallback(() => { const showField = useCallback(() => {
if (onClickShowField) { if (onClickShowField) {
onClickShowField(keys[0]); onClickShowField(keys[0]);
} }
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', { reportInteractionWrapper('logs_log_line_details_show_field_clicked', {
datasourceType: log.datasourceType, datasourceType: log.datasourceType,
logRowUid: log.uid,
type: 'enable',
}); });
}, [onClickShowField, keys, log.datasourceType, log.uid]); }, [onClickShowField, reportInteractionWrapper, log.datasourceType, keys]);
const hideField = useCallback(() => { const hideField = useCallback(() => {
if (onClickHideField) { if (onClickHideField) {
onClickHideField(keys[0]); onClickHideField(keys[0]);
} }
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', { reportInteractionWrapper('logs_log_line_details_hide_field_clicked', {
datasourceType: log.datasourceType, datasourceType: log.datasourceType,
logRowUid: log.uid,
type: 'disable',
}); });
}, [onClickHideField, keys, log.datasourceType, log.uid]); }, [onClickHideField, reportInteractionWrapper, log.datasourceType, keys]);
const filterLabel = useCallback(() => { const filterLabel = useCallback(() => {
if (onClickFilterLabel) { if (onClickFilterLabel) {
onClickFilterLabel(keys[0], values[0], logRowToSingleRowDataFrame(log) || undefined); onClickFilterLabel(keys[0], values[0], logRowToSingleRowDataFrame(log) || undefined);
} }
reportInteraction('grafana_explore_logs_log_details_filter_clicked', { reportInteractionWrapper('logs_log_line_details_filter_clicked', {
datasourceType: log.datasourceType, datasourceType: log.datasourceType,
filterType: 'include', filterType: 'include',
logRowUid: log.uid, logRowUid: log.uid,
}); });
}, [onClickFilterLabel, keys, values, log]); }, [onClickFilterLabel, reportInteractionWrapper, log, keys, values]);
const filterOutLabel = useCallback(() => { const filterOutLabel = useCallback(() => {
if (onClickFilterOutLabel) { if (onClickFilterOutLabel) {
onClickFilterOutLabel(keys[0], values[0], logRowToSingleRowDataFrame(log) || undefined); onClickFilterOutLabel(keys[0], values[0], logRowToSingleRowDataFrame(log) || undefined);
} }
reportInteraction('grafana_explore_logs_log_details_filter_clicked', { reportInteractionWrapper('logs_log_line_details_filter_clicked', {
datasourceType: log.datasourceType, datasourceType: log.datasourceType,
filterType: 'exclude', filterType: 'exclude',
logRowUid: log.uid, logRowUid: log.uid,
}); });
}, [onClickFilterOutLabel, keys, values, log]); }, [onClickFilterOutLabel, reportInteractionWrapper, log, keys, values]);
const labelFilterActive = useCallback(async () => { const labelFilterActive = useCallback(async () => {
if (isLabelFilterActive) { if (isLabelFilterActive) {
@ -237,14 +244,14 @@ export const LogLineDetailsField = ({
const showStats = useCallback(() => { const showStats = useCallback(() => {
setShowFieldStats((showFieldStats: boolean) => !showFieldStats); setShowFieldStats((showFieldStats: boolean) => !showFieldStats);
reportInteraction('grafana_explore_logs_log_details_stats_clicked', { reportInteractionWrapper('logs_log_line_details_stats_clicked', {
dataSourceType: log.datasourceType, dataSourceType: log.datasourceType,
fieldType: isLabel ? 'label' : 'detectedField', fieldType: isLabel ? 'label' : 'field',
type: showFieldsStats ? 'close' : 'open', type: showFieldsStats ? 'close' : 'open',
logRowUid: log.uid, logRowUid: log.uid,
app, app,
}); });
}, [app, isLabel, log.datasourceType, log.uid, showFieldsStats]); }, [app, isLabel, log.datasourceType, log.uid, reportInteractionWrapper, showFieldsStats]);
const refIdTooltip = useMemo( const refIdTooltip = useMemo(
() => (app === CoreApp.Explore && log.dataFrame?.refId ? ` in query ${log.dataFrame?.refId}` : ''), () => (app === CoreApp.Explore && log.dataFrame?.refId ? ` in query ${log.dataFrame?.refId}` : ''),

@ -3,6 +3,7 @@ import { useCallback, useMemo, MouseEvent, useRef, ChangeEvent } from 'react';
import { colorManipulator, GrafanaTheme2, LogRowModel, store } from '@grafana/data'; import { colorManipulator, GrafanaTheme2, LogRowModel, store } from '@grafana/data';
import { t } from '@grafana/i18n'; import { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { IconButton, Input, useStyles2 } from '@grafana/ui'; import { IconButton, Input, useStyles2 } from '@grafana/ui';
import { copyText, handleOpenLogsContextClick } from '../../utils'; import { copyText, handleOpenLogsContextClick } from '../../utils';
@ -26,6 +27,7 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
getRowContextQuery, getRowContextQuery,
logOptionsStorageKey, logOptionsStorageKey,
logSupportsContext, logSupportsContext,
noInteractions,
setDetailsMode, setDetailsMode,
onClickHideField, onClickHideField,
onClickShowField, onClickShowField,
@ -39,14 +41,27 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
const styles = useStyles2(getStyles, detailsMode, wrapLogMessage); const styles = useStyles2(getStyles, detailsMode, wrapLogMessage);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const searchUsedRef = useRef(false);
const reportInteractionWrapper = useCallback(
(interactionName: string, properties?: Record<string, unknown>) => {
if (noInteractions) {
return;
}
reportInteraction(interactionName, properties);
},
[noInteractions]
);
const copyLogLine = useCallback(() => { const copyLogLine = useCallback(() => {
copyText(log.entry, containerRef); copyText(log.entry, containerRef);
}, [log.entry]); reportInteractionWrapper('logs_log_line_details_header_copy_clicked');
}, [log.entry, reportInteractionWrapper]);
const copyLinkToLogLine = useCallback(() => { const copyLinkToLogLine = useCallback(() => {
onPermalinkClick?.(log); onPermalinkClick?.(log);
}, [log, onPermalinkClick]); reportInteractionWrapper('logs_log_line_details_header_permalink_clicked');
}, [log, onPermalinkClick, reportInteractionWrapper]);
const togglePinning = useCallback(() => { const togglePinning = useCallback(() => {
if (pinned) { if (pinned) {
@ -54,7 +69,8 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
} else { } else {
onPinLine?.(log); onPinLine?.(log);
} }
}, [log, onPinLine, onUnpinLine, pinned]); reportInteractionWrapper('logs_log_line_details_header_pinning_clicked');
}, [log, onPinLine, onUnpinLine, pinned, reportInteractionWrapper]);
const shouldlogSupportsContext = useMemo( const shouldlogSupportsContext = useMemo(
() => (logSupportsContext ? logSupportsContext(log) : false), () => (logSupportsContext ? logSupportsContext(log) : false),
@ -64,8 +80,9 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
const showContext = useCallback( const showContext = useCallback(
async (event: MouseEvent<HTMLElement>) => { async (event: MouseEvent<HTMLElement>) => {
handleOpenLogsContextClick(event, log, getRowContextQuery, (log: LogRowModel) => onOpenContext?.(log, () => {})); handleOpenLogsContextClick(event, log, getRowContextQuery, (log: LogRowModel) => onOpenContext?.(log, () => {}));
reportInteractionWrapper('logs_log_line_details_header_context_clicked');
}, },
[onOpenContext, getRowContextQuery, log] [log, getRowContextQuery, reportInteractionWrapper, onOpenContext]
); );
const showLogLineToggle = onClickHideField && onClickShowField && displayedFields.length > 0; const showLogLineToggle = onClickHideField && onClickShowField && displayedFields.length > 0;
@ -86,7 +103,8 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
} else { } else {
onClickShowField?.(LOG_LINE_BODY_FIELD_NAME); onClickShowField?.(LOG_LINE_BODY_FIELD_NAME);
} }
}, [logLineDisplayed, onClickHideField, onClickShowField]); reportInteractionWrapper('logs_log_line_details_header_show_logline_clicked');
}, [logLineDisplayed, onClickHideField, onClickShowField, reportInteractionWrapper]);
const clearSearch = useMemo( const clearSearch = useMemo(
() => ( () => (
@ -95,6 +113,7 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
size="sm" size="sm"
onClick={() => { onClick={() => {
onSearch(''); onSearch('');
reportInteractionWrapper('logs_log_line_details_header_search_cleared');
if (inputRef.current) { if (inputRef.current) {
inputRef.current.value = ''; inputRef.current.value = '';
} }
@ -102,14 +121,18 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
tooltip={t('logs.log-line-details.clear-search', 'Clear')} tooltip={t('logs.log-line-details.clear-search', 'Clear')}
/> />
), ),
[onSearch] [onSearch, reportInteractionWrapper]
); );
const handleSearch = useCallback( const handleSearch = useCallback(
(e: ChangeEvent<HTMLInputElement>) => { (e: ChangeEvent<HTMLInputElement>) => {
onSearch(e.target.value); onSearch(e.target.value);
if (!searchUsedRef.current) {
reportInteractionWrapper('logs_log_line_details_header_search_used');
searchUsedRef.current = true;
}
}, },
[onSearch] [onSearch, reportInteractionWrapper]
); );
return ( return (

@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data'; import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils'; import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils';
import { createLogRow } from '../mocks/logRow'; import { createLogRow } from '../mocks/logRow';
@ -12,6 +13,7 @@ jest.mock('@grafana/runtime', () => {
return { return {
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
usePluginLinks: jest.fn().mockReturnValue({ links: [] }), usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
reportInteraction: jest.fn(),
config: { config: {
...jest.requireActual('@grafana/runtime').config, ...jest.requireActual('@grafana/runtime').config,
featureToggles: { featureToggles: {
@ -338,4 +340,20 @@ describe('LogList', () => {
expect(screen.getByText('some text')).toBeInTheDocument(); expect(screen.getByText('some text')).toBeInTheDocument();
}); });
}); });
describe('Interactions', () => {
beforeEach(() => {
sessionStorage.clear();
jest.mocked(reportInteraction).mockClear();
});
test('Reports interactions ', async () => {
render(<LogList {...defaultProps} />);
await screen.findByText('log message 1');
expect(reportInteraction).toHaveBeenCalled();
});
test('Can disable interaction report ', async () => {
render(<LogList {...defaultProps} noInteractions={true} />);
await screen.findByText('log message 1');
expect(reportInteraction).not.toHaveBeenCalled();
});
});
}); });

@ -59,6 +59,7 @@ export interface Props {
logs: LogRowModel[]; logs: LogRowModel[];
logsMeta?: LogsMetaItem[]; logsMeta?: LogsMetaItem[];
logSupportsContext?: (row: LogRowModel) => boolean; logSupportsContext?: (row: LogRowModel) => boolean;
noInteractions?: boolean;
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterString?: (value: string, refId?: string) => void; onClickFilterString?: (value: string, refId?: string) => void;
@ -124,6 +125,7 @@ export const LogList = ({
logs, logs,
logsMeta, logsMeta,
logSupportsContext, logSupportsContext,
noInteractions,
onClickFilterLabel, onClickFilterLabel,
onClickFilterOutLabel, onClickFilterOutLabel,
onClickFilterString, onClickFilterString,
@ -165,6 +167,7 @@ export const LogList = ({
logLineMenuCustomItems={logLineMenuCustomItems} logLineMenuCustomItems={logLineMenuCustomItems}
logOptionsStorageKey={logOptionsStorageKey} logOptionsStorageKey={logOptionsStorageKey}
logSupportsContext={logSupportsContext} logSupportsContext={logSupportsContext}
noInteractions={noInteractions}
onClickFilterLabel={onClickFilterLabel} onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel} onClickFilterOutLabel={onClickFilterOutLabel}
onClickFilterString={onClickFilterString} onClickFilterString={onClickFilterString}

@ -22,7 +22,7 @@ import {
shallowCompare, shallowCompare,
store, store,
} from '@grafana/data'; } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { PopoverContent } from '@grafana/ui'; import { PopoverContent } from '@grafana/ui';
import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils'; import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils';
@ -79,6 +79,7 @@ export const LogListContext = createContext<LogListContextData>({
forceEscape: false, forceEscape: false,
fontSize: 'default', fontSize: 'default',
hasUnescapedContent: false, hasUnescapedContent: false,
noInteractions: false,
setDedupStrategy: () => {}, setDedupStrategy: () => {},
setDetailsMode: () => {}, setDetailsMode: () => {},
setDetailsWidth: () => {}, setDetailsWidth: () => {},
@ -153,6 +154,7 @@ export interface Props {
logsMeta?: LogsMetaItem[]; logsMeta?: LogsMetaItem[];
logOptionsStorageKey?: string; logOptionsStorageKey?: string;
logSupportsContext?: (row: LogRowModel) => boolean; logSupportsContext?: (row: LogRowModel) => boolean;
noInteractions?: boolean;
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterString?: (value: string, refId?: string) => void; onClickFilterString?: (value: string, refId?: string) => void;
@ -195,6 +197,7 @@ export const LogListContextProvider = ({
logsMeta, logsMeta,
logOptionsStorageKey, logOptionsStorageKey,
logSupportsContext, logSupportsContext,
noInteractions,
onClickFilterLabel, onClickFilterLabel,
onClickFilterOutLabel, onClickFilterOutLabel,
onClickFilterString, onClickFilterString,
@ -237,6 +240,25 @@ export const LogListContextProvider = ({
const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey)); const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey));
const [detailsMode, setDetailsMode] = useState<LogLineDetailsMode>(detailsModeProp ?? 'sidebar'); const [detailsMode, setDetailsMode] = useState<LogLineDetailsMode>(detailsModeProp ?? 'sidebar');
useEffect(() => {
if (noInteractions) {
return;
}
reportInteractionOnce(`logs_log_list_${app}_logs_displayed`, {
dedupStrategy,
fontSize,
forceEscape: logListState.forceEscape,
showTime,
syntaxHighlighting,
wrapLogMessage,
detailsWidth,
detailsMode,
withDisplayedFields: displayedFields.length > 0,
});
// Just once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
if (displayedFields.length > 0 || !config.featureToggles.otelLogsFormatting || !setDisplayedFields) { if (displayedFields.length > 0 || !config.featureToggles.otelLogsFormatting || !setDisplayedFields) {
return; return;
@ -510,6 +532,7 @@ export const LogListContextProvider = ({
logSupportsContext, logSupportsContext,
logLineMenuCustomItems, logLineMenuCustomItems,
logOptionsStorageKey, logOptionsStorageKey,
noInteractions: noInteractions ?? false,
onClickFilterLabel, onClickFilterLabel,
onClickFilterOutLabel, onClickFilterOutLabel,
onClickFilterString, onClickFilterString,
@ -605,3 +628,12 @@ export function getDetailsScrollPosition(log: LogListModel) {
export function removeDetailsScrollPosition(log: LogListModel) { export function removeDetailsScrollPosition(log: LogListModel) {
detailsScrollMap.delete(log.uid); detailsScrollMap.delete(log.uid);
} }
const reportInteractionOnce = (interactionName: string, properties?: Record<string, unknown>) => {
const key = `logs.log-list-context.events.${interactionName}`;
if (sessionStorage.getItem(key)) {
return;
}
sessionStorage.setItem(key, '1');
reportInteraction(interactionName, properties);
};

@ -4,6 +4,7 @@ import { VariableSizeList } from 'react-window';
import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data'; import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data';
import { t } from '@grafana/i18n'; import { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { IconButton, Input, useStyles2 } from '@grafana/ui'; import { IconButton, Input, useStyles2 } from '@grafana/ui';
import { useLogListContext } from './LogListContext'; import { useLogListContext } from './LogListContext';
@ -26,10 +27,11 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
searchVisible, searchVisible,
toggleFilterLogs, toggleFilterLogs,
} = useLogListSearchContext(); } = useLogListSearchContext();
const { displayedFields } = useLogListContext(); const { displayedFields, noInteractions } = useLogListContext();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [currentResult, setCurrentResult] = useState<number | null>(null); const [currentResult, setCurrentResult] = useState<number | null>(null);
const inputRef = useRef(''); const inputRef = useRef('');
const searchUsedRef = useRef(false);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const matches = useMemo(() => { const matches = useMemo(() => {
@ -39,12 +41,19 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
return findMatchingLogs(logs, search, displayedFields); return findMatchingLogs(logs, search, displayedFields);
}, [displayedFields, logs, search, searchVisible]); }, [displayedFields, logs, search, searchVisible]);
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleChange = useCallback(
inputRef.current = e.target.value; (e: ChangeEvent<HTMLInputElement>) => {
startTransition(() => { inputRef.current = e.target.value;
setSearch(inputRef.current); startTransition(() => {
}); setSearch(inputRef.current);
}, []); });
if (!searchUsedRef.current && !noInteractions) {
reportInteraction('logs_log_list_search_used');
searchUsedRef.current = true;
}
},
[noInteractions]
);
const prevResult = useCallback(() => { const prevResult = useCallback(() => {
if (currentResult === null) { if (currentResult === null) {

@ -164,6 +164,7 @@ export const LogsPanel = ({
fontSize, fontSize,
syntaxHighlighting, syntaxHighlighting,
detailsMode: detailsModeProp, detailsMode: detailsModeProp,
noInteractions,
...options ...options
}, },
id, id,
@ -566,6 +567,7 @@ export const LogsPanel = ({
logs={deduplicatedRows} logs={deduplicatedRows}
logSupportsContext={showContextToggle} logSupportsContext={showContextToggle}
loadMore={enableInfiniteScrolling ? loadMoreLogs : undefined} loadMore={enableInfiniteScrolling ? loadMoreLogs : undefined}
noInteractions={noInteractions}
onClickFilterLabel={ onClickFilterLabel={
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
} }

@ -39,6 +39,7 @@ composableKinds: PanelCfg: {
sortOrder: common.LogsSortOrder sortOrder: common.LogsSortOrder
dedupStrategy: common.LogsDedupStrategy dedupStrategy: common.LogsDedupStrategy
enableInfiniteScrolling?: bool enableInfiniteScrolling?: bool
noInteractions?: bool
fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small") fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small")
detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar") detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar")
// TODO: figure out how to define callbacks // TODO: figure out how to define callbacks

@ -22,6 +22,7 @@ export interface Options {
logLineMenuCustomItems?: unknown; logLineMenuCustomItems?: unknown;
logRowMenuIconsAfter?: unknown; logRowMenuIconsAfter?: unknown;
logRowMenuIconsBefore?: unknown; logRowMenuIconsBefore?: unknown;
noInteractions?: boolean;
/** /**
* TODO: figure out how to define callbacks * TODO: figure out how to define callbacks
*/ */

Loading…
Cancel
Save