diff --git a/static/app/views/explore/components/chart/chartFooter.tsx b/static/app/views/explore/components/chart/chartFooter.tsx new file mode 100644 index 00000000000000..9850ae907170a3 --- /dev/null +++ b/static/app/views/explore/components/chart/chartFooter.tsx @@ -0,0 +1,13 @@ +import styled from '@emotion/styled'; + +import usePrevious from 'sentry/utils/usePrevious'; + +export function usePreviouslyLoaded(current: T, isLoading: boolean): T { + const previous = usePrevious(current, isLoading); + return isLoading ? previous : current; +} + +export const Container = styled('span')` + color: ${p => p.theme.subText}; + font-size: ${p => p.theme.fontSize.sm}; +`; diff --git a/static/app/views/explore/components/chart/types.tsx b/static/app/views/explore/components/chart/types.tsx index 7180f665680b52..c272f885a856c3 100644 --- a/static/app/views/explore/components/chart/types.tsx +++ b/static/app/views/explore/components/chart/types.tsx @@ -14,4 +14,5 @@ export interface ChartInfo { isSampled?: boolean | null; sampleCount?: number; samplingMode?: SamplingMode; + topEvents?: number; } diff --git a/static/app/views/explore/logs/confidenceFooter.tsx b/static/app/views/explore/logs/confidenceFooter.tsx new file mode 100644 index 00000000000000..09252f3d0dd5d7 --- /dev/null +++ b/static/app/views/explore/logs/confidenceFooter.tsx @@ -0,0 +1,144 @@ +import {Tooltip} from 'sentry/components/core/tooltip'; +import Count from 'sentry/components/count'; +import {t, tct} from 'sentry/locale'; +import type {Confidence} from 'sentry/types/organization'; +import {defined} from 'sentry/utils'; +import { + Container, + usePreviouslyLoaded, +} from 'sentry/views/explore/components/chart/chartFooter'; +import type {ChartInfo} from 'sentry/views/explore/components/chart/types'; + +interface ConfidenceFooterProps { + chartInfo: ChartInfo; + isLoading: boolean; +} + +export function ConfidenceFooter({ + chartInfo: currentChartInfo, + isLoading, +}: ConfidenceFooterProps) { + const chartInfo = usePreviouslyLoaded(currentChartInfo, isLoading); + return ( + + + + ); +} + +interface ConfidenceMessageProps { + confidence?: Confidence; + dataScanned?: 'full' | 'partial'; + isSampled?: boolean | null; + sampleCount?: number; + topEvents?: number; +} + +function ConfidenceMessage({ + sampleCount, + dataScanned, + confidence, + topEvents, + isSampled, +}: ConfidenceMessageProps) { + const isTopN = defined(topEvents) && topEvents > 1; + + if (!defined(sampleCount)) { + return isTopN + ? t('* Top %s groups extrapolated based on \u2026', topEvents) + : t('* Extrapolated based on \u2026'); + } + + const noSampling = defined(isSampled) && !isSampled; + const sampleCountComponent = ; + + if (dataScanned === 'full') { + // For logs, if the full data was scanned, we can assume that no + // extrapolation happened and we should remove mentions of extrapolation. + if (isTopN) { + return tct('Top [topEvents] groups based on [sampleCountComponent] logs', { + topEvents, + sampleCountComponent, + }); + } + + return tct('Based on [sampleCountComponent] logs', { + sampleCountComponent, + }); + } + + if (confidence === 'low') { + const lowAccuracyFullSampleCount = ; + + if (isTopN) { + return tct( + 'Top [topEvents] groups extrapolated based on [tooltip:[sampleCountComponent] logs]', + { + topEvents, + tooltip: lowAccuracyFullSampleCount, + sampleCountComponent, + } + ); + } + + return tct('Extrapolated based on [tooltip:[sampleCountComponent] logs]', { + tooltip: lowAccuracyFullSampleCount, + sampleCountComponent, + }); + } + + if (isTopN) { + return tct( + 'Top [topEvents] groups extrapolated based on [sampleCountComponent] logs', + { + topEvents, + sampleCountComponent, + } + ); + } + + return tct('Extrapolated based on [sampleCountComponent] logs', { + sampleCountComponent, + }); +} + +function LowAccuracyFullTooltip({ + noSampling, + children, +}: { + noSampling: boolean; + children?: React.ReactNode; +}) { + return ( + + {t( + 'You may not have enough logs for a high accuracy extrapolation of your query.' + )} +
+
+ {t( + "You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval." + )} +
+
+ {t( + 'You can also increase your sampling rates to get more samples and accurate trends.' + )} + + } + disabled={noSampling} + maxWidth={270} + showUnderline + > + {children} +
+ ); +} diff --git a/static/app/views/explore/logs/logsGraph.tsx b/static/app/views/explore/logs/logsGraph.tsx index 28360adbf495a8..af1e99b54fdb94 100644 --- a/static/app/views/explore/logs/logsGraph.tsx +++ b/static/app/views/explore/logs/logsGraph.tsx @@ -4,15 +4,25 @@ import {CompactSelect} from 'sentry/components/core/compactSelect'; import {Tooltip} from 'sentry/components/core/tooltip'; import {IconClock, IconGraph} from 'sentry/icons'; import {t} from 'sentry/locale'; +import {defined} from 'sentry/utils'; +import {determineSeriesSampleCountAndIsSampled} from 'sentry/views/alerts/rules/metric/utils/determineSeriesSampleCount'; import {Widget} from 'sentry/views/dashboards/widgets/widget/widget'; import {ChartVisualization} from 'sentry/views/explore/components/chart/chartVisualization'; -import {useLogsAggregate} from 'sentry/views/explore/contexts/logs/logsPageParams'; +import { + useLogsAggregate, + useLogsGroupBy, +} from 'sentry/views/explore/contexts/logs/logsPageParams'; import { ChartIntervalUnspecifiedStrategy, useChartInterval, } from 'sentry/views/explore/hooks/useChartInterval'; +import {TOP_EVENTS_LIMIT} from 'sentry/views/explore/hooks/useTopEvents'; +import {ConfidenceFooter} from 'sentry/views/explore/logs/confidenceFooter'; import {EXPLORE_CHART_TYPE_OPTIONS} from 'sentry/views/explore/spans/charts'; -import {prettifyAggregation} from 'sentry/views/explore/utils'; +import { + combineConfidenceForSeries, + prettifyAggregation, +} from 'sentry/views/explore/utils'; import {ChartType} from 'sentry/views/insights/common/components/chart'; import type {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries'; @@ -22,6 +32,7 @@ interface LogsGraphProps { export function LogsGraph({timeseriesResult}: LogsGraphProps) { const aggregate = useLogsAggregate(); + const groupBy = useLogsGroupBy(); const [chartType, setChartType] = useState(ChartType.BAR); const [interval, setInterval, intervalOptions] = useChartInterval({ @@ -30,13 +41,21 @@ export function LogsGraph({timeseriesResult}: LogsGraphProps) { const chartInfo = useMemo(() => { const series = timeseriesResult.data[aggregate] ?? []; + const isTopEvents = defined(groupBy); + const samplingMeta = determineSeriesSampleCountAndIsSampled(series, isTopEvents); return { chartType, series, timeseriesResult, yAxis: aggregate, + confidence: combineConfidenceForSeries(series), + dataScanned: samplingMeta.dataScanned, + isSampled: samplingMeta.isSampled, + sampleCount: samplingMeta.sampleCount, + samplingMode: undefined, + topEvents: isTopEvents ? TOP_EVENTS_LIMIT : undefined, }; - }, [chartType, timeseriesResult, aggregate]); + }, [chartType, timeseriesResult, aggregate, groupBy]); const Title = ( @@ -83,6 +102,9 @@ export function LogsGraph({timeseriesResult}: LogsGraphProps) { Title={Title} Actions={Actions} Visualization={} + Footer={ + + } revealActions="always" /> ); diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index cf0c18dacbb955..bae9f3dd1b7ace 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -67,6 +67,7 @@ import { ChartIntervalUnspecifiedStrategy, useChartInterval, } from 'sentry/views/explore/hooks/useChartInterval'; +import {TOP_EVENTS_LIMIT} from 'sentry/views/explore/hooks/useTopEvents'; import { HiddenColumnEditorLogFields, HiddenLogSearchFields, @@ -171,7 +172,7 @@ export function LogsTabContent({ yAxis: [aggregate], interval, fields: [...(groupBy ? [groupBy] : []), aggregate], - topEvents: groupBy?.length ? 5 : undefined, + topEvents: groupBy ? TOP_EVENTS_LIMIT : undefined, orderby, }, 'explore.ourlogs.main-chart',