From 3e1c7da9f04e52426ef1c1bc6829c4128304435d Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 19 Nov 2025 15:45:54 -0500 Subject: [PATCH 1/5] feat(explore): Update confidence footer again This update follows a few philosophies: 1. use the number matched, number scanned, number total to inform the user 2. give as much information to the user as possible about what was done to compute the results 3. never display the same number more than once as it is repetitive/redundant --- .../spans/charts/confidenceFooter.spec.tsx | 872 ++++++++++++++++-- .../explore/spans/charts/confidenceFooter.tsx | 231 +++-- 2 files changed, 961 insertions(+), 142 deletions(-) diff --git a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx index bedb365866c2e1..c857eb60fce618 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx @@ -7,105 +7,831 @@ function Wrapper({children}: {children: React.ReactNode}) { } describe('ConfidenceFooter', () => { - describe('low confidence', () => { - it('renders for full scan without grouping', async () => { - render( - , - {wrapper: Wrapper} - ); + it('loading', () => { + render(, {wrapper: Wrapper}); + expect(screen.getByTestId('loading-placeholder')).toBeInTheDocument(); + }); - expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Extrapolated from 100 spans' - ); - await userEvent.hover(screen.getByText('100')); - expect( - await screen.findByText( - /You may not have enough span samples for a high accuracy extrapolation of your query./ - ) - ).toBeInTheDocument(); + describe('unextrapolated', () => { + it('loaded without top events', () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); }); - it('renders for full scan with grouping', async () => { - render( - , - {wrapper: Wrapper} - ); + it('loaded with top events', () => { + render(, { + wrapper: Wrapper, + }); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Extrapolated from 100 spans for top 5 groups' + 'Span count for top 5 groups: 100' ); - await userEvent.hover(screen.getByText('100')); - expect( - await screen.findByText( - /You may not have enough span samples for a high accuracy extrapolation of your query./ - ) - ).toBeInTheDocument(); }); }); - describe('high confidence', () => { - it('renders for full scan without grouping', () => { - render( - , - {wrapper: Wrapper} - ); - + describe('unsampled', () => { + it('loaded without top events', () => { + render(, { + wrapper: Wrapper, + }); expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); }); - it('renders for full scan with grouping', () => { - render( - , - {wrapper: Wrapper} - ); + it('loaded with top events', () => { + render(, { + wrapper: Wrapper, + }); expect(screen.getByTestId('wrapper')).toHaveTextContent( 'Span count for top 5 groups: 100' ); }); }); - describe('unextrapolated', () => { - it('unextrapolated loading', () => { - render(, {wrapper: Wrapper}); + describe('without raw counts', () => { + describe('low confidence', () => { + it('loaded 1', async () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent('Estimated from 1 span'); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 span') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 span' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 span') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); - expect(screen.getByTestId('loading-placeholder')).toBeInTheDocument(); + it('loaded 10', async () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 spans') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 spans') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); }); - it('unextrapolated loaded', () => { - render(, { - wrapper: Wrapper, + describe('high confidence', () => { + it('loaded 1', () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent('Estimated from 1 span'); }); - expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); + it('loaded 1 with grouping', () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 span' + ); + }); + + it('loaded 10', () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 spans' + ); + }); + + it('loaded 10 with grouping', () => { + render(, { + wrapper: Wrapper, + }); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 spans' + ); + }); }); + }); - it('unextrapolated loaded with grouping', () => { - render(, { - wrapper: Wrapper, + describe('with raw counts', () => { + const rawSpanCounts = { + normal: { + count: 100, + isLoading: false, + }, + highAccuracy: { + count: 1000, + isLoading: false, + }, + }; + + describe('without user query', () => { + describe('partial scan', () => { + describe('low confidence', () => { + it('loaded 1', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 sample of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 sample') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 sample of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 sample') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 samples of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 samples') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 samples of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 samples') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + }); + + describe('high confidence', () => { + it('loaded 1', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 sample of 1k spans' + ); + }); + + it('loaded 1 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 sample of 1k spans' + ); + }); + + it('loaded 10', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 samples of 1k spans' + ); + }); + + it('loaded 10 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 samples of 1k spans' + ); + }); + }); }); - expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100' - ); + describe('full scan', () => { + describe('low confidence', () => { + it('loaded 1', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 span' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 span') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 span' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 span') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 spans') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 spans') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + }); + + describe('high confidence', () => { + it('loaded 1', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 span' + ); + }); + + it('loaded 1 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 span' + ); + }); + + it('loaded 10', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 spans' + ); + }); + + it('loaded 10 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 spans' + ); + }); + }); + }); + }); + + describe('with user query', () => { + describe('partial scan', () => { + describe('low confidence', () => { + it('loaded 1', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 match after scanning 100 samples of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 match') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 match after scanning 100 samples of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 match') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 matches after scanning 100 samples of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 matches') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 matches after scanning 100 samples of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 matches') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + }); + + describe('high confidence', () => { + it('loaded 1', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 match after scanning 100 samples of 1k spans' + ); + }); + + it('loaded 1 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 match after scanning 100 samples of 1k spans' + ); + }); + + it('loaded 10', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 matches after scanning 100 samples of 1k spans' + ); + }); + + it('loaded 10 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 matches after scanning 100 samples of 1k spans' + ); + }); + }); + }); + + describe('full scan', () => { + describe('low confidence', () => { + it('loaded 1', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 match of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 match') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 match of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 match') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 matches of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 matches') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 matches of 1k spans' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 matches') + ); + expect( + await screen.findByText( + /You may not have enough span samples for a high accuracy estimation of your query./ + ) + ).toBeInTheDocument(); + }); + }); + + describe('high confidence', () => { + it('loaded 1', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 match of 1k spans' + ); + }); + + it('loaded 1 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 match of 1k spans' + ); + }); + + it('loaded 10', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 matches of 1k spans' + ); + }); + + it('loaded 10 with grouping', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 matches of 1k spans' + ); + }); + }); + }); }); }); }); diff --git a/static/app/views/explore/spans/charts/confidenceFooter.tsx b/static/app/views/explore/spans/charts/confidenceFooter.tsx index 96db5f43123660..aa859ca3536e87 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.tsx @@ -6,7 +6,6 @@ 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 usePrevious from 'sentry/utils/usePrevious'; import { Placeholder, WarningIcon, @@ -26,13 +25,11 @@ type Props = { }; export function ConfidenceFooter(props: Props) { - const previousProps = usePrevious(props, props.isLoading); - return ( - {confidenceMessage(props.isLoading ? previousProps : props)} - ); + return {confidenceMessage(props)}; } function confidenceMessage({ + dataScanned, extrapolate, rawSpanCounts, sampleCount, @@ -42,19 +39,18 @@ function confidenceMessage({ isLoading, userQuery, }: Props) { - const isTopN = defined(topEvents) && topEvents > 1; - - if (!defined(sampleCount) || isLoading) { + if (isLoading || !defined(sampleCount)) { return ; } + const isTopN = defined(topEvents) && topEvents > 1; + const noSampling = defined(isSampled) && !isSampled; + if ( - // Extrapolation disabled, so don't mention extrapolation. + // Extrapolation disabled, so don't mention estimations. (defined(extrapolate) && !extrapolate) || - // High confidence without user query means we're in a default query. - // We check for high confidence here because we still want to show the - // tooltip here if it's low confidence - (confidence === 'high' && !userQuery) + // No sampling happened, so don't mention estimations. + noSampling ) { return isTopN ? tct('Span count for top [topEvents] groups: [matchingSpansCount]', { @@ -66,111 +62,206 @@ function confidenceMessage({ }); } - const noSampling = defined(isSampled) && !isSampled; - const lowAccuracyFullSampleCount = <_LowAccuracyFullTooltip noSampling={noSampling} />; + const maybeWarning = + confidence === 'low' ? tct('[warning] ', {warning: }) : null; + const maybeTooltip = + confidence === 'low' ? <_LowAccuracyFullTooltip noSampling={noSampling} /> : null; // The multi query mode does not fetch the raw span counts // so make sure to have a backup when this happens. if (!defined(rawSpanCounts)) { const matchingSpansCount = - sampleCount > 1 - ? t('%s spans', ) - : t('%s span', ); + sampleCount === 1 + ? t('%s span', ) + : t('%s spans', ); - if (confidence === 'high') { - if (isTopN) { - return tct('Extrapolated from [matchingSpansCount] for top [topEvents] groups', { + if (isTopN) { + return tct( + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingSpansCount]]', + { + maybeWarning, topEvents, + maybeTooltip, matchingSpansCount, - }); + } + ); + } + + return tct('[maybeWarning]Estimated from [maybeTooltip:[matchingSpansCount]]', { + maybeWarning, + maybeTooltip, + matchingSpansCount, + }); + } + + // no user query means it's showing the total number of spans scanned + // so no need to mention how many matched + if (!userQuery) { + // partial scans means that we didnt scan all the data so it's useful + // to mention the total number of spans available + if (dataScanned === 'partial') { + const matchingSpansCount = + sampleCount > 1 + ? t('%s samples', ) + : t('%s sample', ); + + const totalSpansCount = rawSpanCounts.highAccuracy.count ? ( + rawSpanCounts.highAccuracy.count > 1 ? ( + t('%s spans', ) + ) : ( + t('%s span', ) + ) + ) : ( + + ); + + if (isTopN) { + return tct( + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingSpansCount]] of [totalSpansCount]', + { + maybeWarning, + topEvents, + maybeTooltip, + matchingSpansCount, + totalSpansCount, + } + ); } - return tct('Extrapolated from [matchingSpansCount]', { - matchingSpansCount, - }); + return tct( + '[maybeWarning]Estimated from [maybeTooltip:[matchingSpansCount]] of [totalSpansCount]', + { + maybeWarning, + maybeTooltip, + matchingSpansCount, + totalSpansCount, + } + ); } + // otherwise, a full scan was done + // full scan means we scanned all the data available so no need to repeat that information twice + + const matchingSpansCount = + sampleCount > 1 + ? t('%s spans', ) + : t('%s span', ); + if (isTopN) { return tct( - 'Extrapolated from [tooltip:[matchingSpansCount]] for top [topEvents] groups', + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingSpansCount]]', { + maybeWarning, + maybeTooltip, topEvents, matchingSpansCount, - tooltip: lowAccuracyFullSampleCount, } ); } - return tct('Extrapolated from [tooltip:[matchingSpansCount]]', { + return tct('[maybeWarning]Estimated from [maybeTooltip:[matchingSpansCount]]', { + maybeWarning, + maybeTooltip, matchingSpansCount, - tooltip: lowAccuracyFullSampleCount, }); } - const matchingSpansCount = - sampleCount > 1 - ? t('%s matches', ) - : t('%s match', ); + // otherwise, a user query was specified + // with a user query, it means we should tell the user how many of the scanned spans + // matched the user query - const downSampledSpansCount = rawSpanCounts.normal.count ? ( - rawSpanCounts.normal.count > 1 ? ( - t('%s samples', ) + // partial scans means that we didnt scan all the data so it's useful + // to mention the total number of spans available + if (dataScanned === 'partial') { + const matchingSpansCount = + sampleCount > 1 + ? t('%s matches', ) + : t('%s match', ); + + const scannedSpansCount = rawSpanCounts.normal.count ? ( + rawSpanCounts.normal.count > 1 ? ( + t('%s samples', ) + ) : ( + t('%s sample', ) + ) ) : ( - t('%s sample', ) - ) - ) : ( - - ); - const allSpansCount = rawSpanCounts.highAccuracy.count ? ( - rawSpanCounts.highAccuracy.count > 1 ? ( - t('%s spans', ) + + ); + + const totalSpansCount = rawSpanCounts.highAccuracy.count ? ( + rawSpanCounts.highAccuracy.count > 1 ? ( + t('%s spans', ) + ) : ( + t('%s span', ) + ) ) : ( - t('%s span', ) - ) - ) : ( - - ); + + ); - if (confidence === 'high') { if (isTopN) { return tct( - 'Extrapolated from [matchingSpansCount] for top [topEvents] groups in [allSpansCount]', + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingSpansCount]] after scanning [scannedSpansCount] of [totalSpansCount]', { - topEvents, + maybeWarning, + maybeTooltip, matchingSpansCount, - allSpansCount, + scannedSpansCount, + totalSpansCount, + topEvents, } ); } - return tct('Extrapolated from [matchingSpansCount] in [allSpansCount]', { - matchingSpansCount, - allSpansCount, - }); + return tct( + '[maybeWarning]Estimated from [maybeTooltip:[matchingSpansCount]] after scanning [scannedSpansCount] of [totalSpansCount]', + { + maybeWarning, + maybeTooltip, + matchingSpansCount, + scannedSpansCount, + totalSpansCount, + } + ); } + // otherwise, a full scan was done + // full scan means we scanned all the data available so no need to repeat that information twice + + const matchingSpansCount = + sampleCount > 1 + ? t('%s matches', ) + : t('%s match', ); + + const totalSpansCount = rawSpanCounts.highAccuracy.count ? ( + rawSpanCounts.highAccuracy.count > 1 ? ( + t('%s spans', ) + ) : ( + t('%s span', ) + ) + ) : ( + + ); + if (isTopN) { return tct( - '[warning] Extrapolated from [matchingSpansCount] for top [topEvents] groups after scanning [tooltip:[downSampledSpansCount] of [allSpansCount]]', + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingSpansCount]] of [totalSpansCount]', { - warning: , + maybeWarning, topEvents, + maybeTooltip, matchingSpansCount, - downSampledSpansCount, - allSpansCount, - tooltip: lowAccuracyFullSampleCount, + totalSpansCount, } ); } return tct( - '[warning] Extrapolated from [matchingSpansCount] after scanning [tooltip:[downSampledSpansCount] of [allSpansCount]]', + '[maybeWarning]Estimated from [maybeTooltip:[matchingSpansCount]] of [totalSpansCount]', { - warning: , + maybeWarning, + maybeTooltip, matchingSpansCount, - downSampledSpansCount, - allSpansCount, - tooltip: lowAccuracyFullSampleCount, + totalSpansCount, } ); } @@ -187,15 +278,17 @@ function _LowAccuracyFullTooltip({ title={
{t( - 'You may not have enough span samples for a high accuracy extrapolation of your query.' + 'You may not have enough span samples for a high accuracy estimation of your query.' )}
+
{t( "You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval." )} {/* Do not show if no sampling happened to the data points in the series as they are already at 100% sampling */} {!noSampling && ( +

{t( 'You can also increase your sampling rates to get more samples and accurate trends.' @@ -204,7 +297,7 @@ function _LowAccuracyFullTooltip({ )}
} - maxWidth={270} + maxWidth={300} showUnderline > {children} From 1a151db45ab521eb10f4b93cb180092e6ed013c2 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 19 Nov 2025 17:44:24 -0500 Subject: [PATCH 2/5] updates to span footers --- .../spans/charts/confidenceFooter.spec.tsx | 180 ++++++++++++++---- .../explore/spans/charts/confidenceFooter.tsx | 67 +++++-- 2 files changed, 195 insertions(+), 52 deletions(-) diff --git a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx index c857eb60fce618..2473795e6356a8 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx @@ -12,42 +12,6 @@ describe('ConfidenceFooter', () => { expect(screen.getByTestId('loading-placeholder')).toBeInTheDocument(); }); - describe('unextrapolated', () => { - it('loaded without top events', () => { - render(, { - wrapper: Wrapper, - }); - expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); - }); - - it('loaded with top events', () => { - render(, { - wrapper: Wrapper, - }); - expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100' - ); - }); - }); - - describe('unsampled', () => { - it('loaded without top events', () => { - render(, { - wrapper: Wrapper, - }); - expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); - }); - - it('loaded with top events', () => { - render(, { - wrapper: Wrapper, - }); - expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100' - ); - }); - }); - describe('without raw counts', () => { describe('low confidence', () => { it('loaded 1', async () => { @@ -166,6 +130,150 @@ describe('ConfidenceFooter', () => { }, }; + describe('unextrapolated', () => { + describe('without user query', () => { + it('loaded without top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); + }); + + it('loaded with top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Span count for top 5 groups: 100' + ); + }); + }); + + describe('with user query', () => { + it('loaded without top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Span count: 100 matches of 1k spans' + ); + }); + + it('loaded with top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Span count for top 5 groups: 100 matches of 1k spans' + ); + }); + }); + }); + + describe('unsampled', () => { + describe('without user query', () => { + it('loaded without top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); + }); + + it('loaded with top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Span count for top 5 groups: 100' + ); + }); + }); + + describe('with user query', () => { + it('loaded without top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Span count: 100 matches of 1k spans' + ); + }); + + it('loaded with top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Span count for top 5 groups: 100 matches of 1k spans' + ); + }); + }); + }); + describe('without user query', () => { describe('partial scan', () => { describe('low confidence', () => { diff --git a/static/app/views/explore/spans/charts/confidenceFooter.tsx b/static/app/views/explore/spans/charts/confidenceFooter.tsx index aa859ca3536e87..257e492540c2ae 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.tsx @@ -46,22 +46,6 @@ function confidenceMessage({ const isTopN = defined(topEvents) && topEvents > 1; const noSampling = defined(isSampled) && !isSampled; - if ( - // Extrapolation disabled, so don't mention estimations. - (defined(extrapolate) && !extrapolate) || - // No sampling happened, so don't mention estimations. - noSampling - ) { - return isTopN - ? tct('Span count for top [topEvents] groups: [matchingSpansCount]', { - topEvents, - matchingSpansCount: , - }) - : tct('Span count: [matchingSpansCount]', { - matchingSpansCount: , - }); - } - const maybeWarning = confidence === 'low' ? tct('[warning] ', {warning: }) : null; const maybeTooltip = @@ -94,6 +78,57 @@ function confidenceMessage({ }); } + if ( + // Extrapolation disabled, so don't mention estimations. + (defined(extrapolate) && !extrapolate) || + // No sampling happened, so don't mention estimations. + noSampling + ) { + if (!userQuery) { + if (isTopN) { + return tct('Span count for top [topEvents] groups: [matchingSpansCount]', { + topEvents, + matchingSpansCount: , + }); + } + + return tct('Span count: [matchingSpansCount]', { + matchingSpansCount: , + }); + } + + const matchingSpansCount = + sampleCount > 1 + ? t('%s matches', ) + : t('%s match', ); + + const totalSpansCount = rawSpanCounts.highAccuracy.count ? ( + rawSpanCounts.highAccuracy.count > 1 ? ( + t('%s spans', ) + ) : ( + t('%s span', ) + ) + ) : ( + + ); + + if (isTopN) { + return tct( + 'Span count for top [topEvents] groups: [matchingSpansCount] of [totalSpansCount]', + { + topEvents, + matchingSpansCount, + totalSpansCount, + } + ); + } + + return tct('Span count: [matchingSpansCount] of [totalSpansCount]', { + matchingSpansCount, + totalSpansCount, + }); + } + // no user query means it's showing the total number of spans scanned // so no need to mention how many matched if (!userQuery) { From df61df1cf02d3d8b64458501518e5aeb8995faba Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 19 Nov 2025 19:45:43 -0500 Subject: [PATCH 3/5] test the tooltip --- .../spans/charts/confidenceFooter.spec.tsx | 100 ++++++++++++++++++ .../explore/spans/charts/confidenceFooter.tsx | 30 +++++- 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx index 2473795e6356a8..67cdf28c887530 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx @@ -27,6 +27,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 1 with grouping', async () => { @@ -44,6 +49,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10', async () => { @@ -61,6 +71,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10 with grouping', async () => { @@ -78,6 +93,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); }); @@ -298,6 +318,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 1 with grouping', async () => { @@ -322,6 +347,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10', async () => { @@ -345,6 +375,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10 with grouping', async () => { @@ -369,6 +404,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); }); @@ -460,6 +500,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 1 with grouping', async () => { @@ -484,6 +529,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10', async () => { @@ -507,6 +557,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10 with grouping', async () => { @@ -531,6 +586,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); }); @@ -625,6 +685,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 1 with grouping', async () => { @@ -650,6 +715,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10', async () => { @@ -674,6 +744,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10 with grouping', async () => { @@ -699,6 +774,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); }); @@ -795,6 +875,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 1 with grouping', async () => { @@ -820,6 +905,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10', async () => { @@ -844,6 +934,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); it('loaded 10 with grouping', async () => { @@ -869,6 +964,11 @@ describe('ConfidenceFooter', () => { /You may not have enough span samples for a high accuracy estimation of your query./ ) ).toBeInTheDocument(); + expect( + await screen.findByText( + /You can try adjusting your query by removing filters or increasing the chart's time interval./ + ) + ).toBeInTheDocument(); }); }); diff --git a/static/app/views/explore/spans/charts/confidenceFooter.tsx b/static/app/views/explore/spans/charts/confidenceFooter.tsx index 257e492540c2ae..bc6186e58ed69f 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.tsx @@ -49,7 +49,13 @@ function confidenceMessage({ const maybeWarning = confidence === 'low' ? tct('[warning] ', {warning: }) : null; const maybeTooltip = - confidence === 'low' ? <_LowAccuracyFullTooltip noSampling={noSampling} /> : null; + confidence === 'low' ? ( + <_LowAccuracyFullTooltip + noSampling={noSampling} + dataScanned={dataScanned} + userQuery={userQuery} + /> + ) : null; // The multi query mode does not fetch the raw span counts // so make sure to have a backup when this happens. @@ -304,9 +310,13 @@ function confidenceMessage({ function _LowAccuracyFullTooltip({ noSampling, children, + dataScanned, + userQuery, }: { noSampling: boolean; children?: React.ReactNode; + dataScanned?: 'full' | 'partial'; + userQuery?: string; }) { return (
- {t( - "You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval." - )} + {dataScanned === 'partial' && userQuery + ? t( + "You can try adjusting your query by narrowing the date range, removing filters or increasing the chart's time interval." + ) + : dataScanned === 'partial' + ? t( + "You can try adjusting your query by narrowing the date range or increasing the chart's time interval." + ) + : userQuery + ? t( + "You can try adjusting your query by removing filters or increasing the chart's time interval." + ) + : t( + "You can try adjusting your query by increasing the chart's time interval." + )} {/* Do not show if no sampling happened to the data points in the series as they are already at 100% sampling */} {!noSampling && ( From a92867062a8c4cf7e954be3645f399916db024d3 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 19 Nov 2025 19:46:02 -0500 Subject: [PATCH 4/5] update logs confidence footer --- .../utils/determineSeriesSampleCount.tsx | 2 +- .../explore/logs/confidenceFooter.spec.tsx | 582 ++++++++++++++++++ .../views/explore/logs/confidenceFooter.tsx | 244 ++++++-- 3 files changed, 775 insertions(+), 53 deletions(-) create mode 100644 static/app/views/explore/logs/confidenceFooter.spec.tsx diff --git a/static/app/views/alerts/rules/metric/utils/determineSeriesSampleCount.tsx b/static/app/views/alerts/rules/metric/utils/determineSeriesSampleCount.tsx index 5d9401391f1308..0bae87c0e287dd 100644 --- a/static/app/views/alerts/rules/metric/utils/determineSeriesSampleCount.tsx +++ b/static/app/views/alerts/rules/metric/utils/determineSeriesSampleCount.tsx @@ -44,7 +44,7 @@ export function determineSeriesSampleCountAndIsSampled( } const sampleRate = data[i]!.values[j]!.sampleRate; - if (sampleRate === 1) { + if (defined(sampleRate) && sampleRate >= 1) { hasUnsampledInterval = true; } else if (defined(sampleRate) && sampleRate < 1) { hasSampledInterval = true; diff --git a/static/app/views/explore/logs/confidenceFooter.spec.tsx b/static/app/views/explore/logs/confidenceFooter.spec.tsx new file mode 100644 index 00000000000000..bef06722f6fd6f --- /dev/null +++ b/static/app/views/explore/logs/confidenceFooter.spec.tsx @@ -0,0 +1,582 @@ +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import type {ChartInfo} from 'sentry/views/explore/components/chart/types'; +import {ChartType} from 'sentry/views/insights/common/components/chart'; + +import {ConfidenceFooter} from './confidenceFooter'; + +function Wrapper({children}: {children: React.ReactNode}) { + return
{children}
; +} + +describe('ConfidenceFooter', () => { + const rawLogCounts = { + normal: { + count: 100, + isLoading: false, + }, + highAccuracy: { + count: 1000, + isLoading: false, + }, + }; + + function chartInfo(info: Partial) { + return { + chartType: ChartType.LINE, + series: [], + timeseriesResult: {} as any, + yAxis: '', + ...info, + }; + } + + it('loading', () => { + render( + , + {wrapper: Wrapper} + ); + expect(screen.getByTestId('loading-placeholder')).toBeInTheDocument(); + }); + + describe('with raw counts', () => { + describe('unsampled', () => { + describe('without user query', () => { + it('loaded without top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent('Log count: 100'); + }); + + it('loaded with top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Log count for top 5 groups: 100' + ); + }); + }); + + describe('with user query', () => { + it('loaded without top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Log count: 100 matches of 1k logs' + ); + }); + + it('loaded with top events', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Log count for top 5 groups: 100 matches of 1k logs' + ); + }); + }); + }); + + describe('sampled', () => { + describe('without user query', () => { + describe('partial scan', () => { + it('loaded 1', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 sample of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 sample') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 sample of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 sample') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 samples of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 samples') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 samples of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 samples') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + }); + + describe('full scan', () => { + it('loaded 1', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 log' + ); + }); + + it('loaded 1 with grouping', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 log' + ); + }); + + it('loaded 10', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 logs' + ); + }); + + it('loaded 10 with grouping', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 logs' + ); + }); + }); + }); + + describe('with user query', () => { + describe('partial scan', () => { + it('loaded 1', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 match after scanning 100 samples of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 match') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 1 with grouping', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 match after scanning 100 samples of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '1 match') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 matches after scanning 100 samples of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 matches') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + + it('loaded 10 with grouping', async () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 matches after scanning 100 samples of 1k logs' + ); + await userEvent.hover( + screen.getByText((_, element) => element?.textContent === '10 matches') + ); + expect( + await screen.findByText( + /The volume of logs in this time range is too large for us to do a full scan./ + ) + ).toBeInTheDocument(); + expect( + await screen.findByText( + /Try reducing the date range or number of projects to attempt scanning all logs./ + ) + ).toBeInTheDocument(); + }); + }); + + describe('full scan', () => { + it('loaded 1', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 1 log' + ); + }); + + it('loaded 1 with grouping', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 1 log' + ); + }); + + it('loaded 10', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated from 10 logs' + ); + }); + + it('loaded 10 with grouping', () => { + render( + , + { + wrapper: Wrapper, + } + ); + expect(screen.getByTestId('wrapper')).toHaveTextContent( + 'Estimated for top 5 groups from 10 logs' + ); + }); + }); + }); + }); + }); +}); diff --git a/static/app/views/explore/logs/confidenceFooter.tsx b/static/app/views/explore/logs/confidenceFooter.tsx index 9b2861ee1e8660..87fdba8b058579 100644 --- a/static/app/views/explore/logs/confidenceFooter.tsx +++ b/static/app/views/explore/logs/confidenceFooter.tsx @@ -3,10 +3,7 @@ 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 {Container} from 'sentry/views/explore/components/chart/chartFooter'; import { Placeholder, WarningIcon, @@ -22,12 +19,11 @@ interface ConfidenceFooterProps { } export function ConfidenceFooter({ - chartInfo: currentChartInfo, + chartInfo, hasUserQuery, isLoading, rawLogCounts, }: ConfidenceFooterProps) { - const chartInfo = usePreviouslyLoaded(currentChartInfo, isLoading); return ( 1; - - if (!defined(sampleCount) || isLoading) { + if (isLoading || !defined(sampleCount)) { return ; } + const isTopN = defined(topEvents) && topEvents > 1; const noSampling = defined(isSampled) && !isSampled; - const matchingLogsCount = - sampleCount > 1 - ? t('%s matches', ) - : t('%s match', ); - const downsampledLogsCount = rawLogCounts.normal.count ? ( - rawLogCounts.normal.count > 1 ? ( - t('%s samples', ) - ) : ( - t('%s sample', ) - ) - ) : ( - - ); - const allLogsCount = rawLogCounts.highAccuracy.count ? ( - rawLogCounts.highAccuracy.count > 1 ? ( - t('%s logs', ) - ) : ( - t('%s log', ) - ) - ) : ( - - ); - if (dataScanned === 'full') { + // No sampling happened, so don't mention estimations. + if (noSampling) { if (!hasUserQuery) { if (isTopN) { return tct('Log count for top [topEvents] groups: [matchingLogsCount]', { @@ -109,46 +83,211 @@ function ConfidenceMessage({ }); } - // For logs, if the full data was scanned, we can assume that no - // extrapolation happened and we should remove mentions of extrapolation. + const matchingLogsCount = + sampleCount > 1 + ? t('%s matches', ) + : t('%s match', ); + + const totalLogsCount = rawLogCounts.highAccuracy.count ? ( + rawLogCounts.highAccuracy.count > 1 ? ( + t('%s logs', ) + ) : ( + t('%s log', ) + ) + ) : ( + + ); + if (isTopN) { - return tct('[matchingLogsCount] for top [topEvents] groups in [allLogsCount]', { - topEvents, - matchingLogsCount, - allLogsCount, - }); + return tct( + 'Log count for top [topEvents] groups: [matchingLogsCount] of [totalLogsCount]', + { + topEvents, + matchingLogsCount, + totalLogsCount, + } + ); } - return tct('[matchingLogsCount] in [allLogsCount]', { + return tct('Log count: [matchingLogsCount] of [totalLogsCount]', { matchingLogsCount, - allLogsCount, + totalLogsCount, }); } - const downsampledTooltip = ; + const maybeWarning = + dataScanned === 'partial' ? tct('[warning] ', {warning: }) : null; + const maybeTooltip = + dataScanned === 'partial' ? : null; + + // no user query means it's showing the total number of logs scanned + // so no need to mention how many matched + if (!hasUserQuery) { + // partial scans means that we didnt scan all the data so it's useful + // to mention the total number of logs available + if (dataScanned === 'partial') { + const matchingLogsCount = + sampleCount > 1 + ? t('%s samples', ) + : t('%s sample', ); + + const totalLogsCount = rawLogCounts.highAccuracy.count ? ( + rawLogCounts.highAccuracy.count > 1 ? ( + t('%s logs', ) + ) : ( + t('%s log', ) + ) + ) : ( + + ); + + if (isTopN) { + return tct( + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingLogsCount]] of [totalLogsCount]', + { + maybeWarning, + topEvents, + maybeTooltip, + matchingLogsCount, + totalLogsCount, + } + ); + } + + return tct( + '[maybeWarning]Estimated from [maybeTooltip:[matchingLogsCount]] of [totalLogsCount]', + { + maybeWarning, + maybeTooltip, + matchingLogsCount, + totalLogsCount, + } + ); + } + + // otherwise, a full scan was done + // full scan means we scanned all the data available so no need to repeat that information twice + + const matchingLogsCount = + sampleCount > 1 + ? t('%s logs', ) + : t('%s log', ); + + if (isTopN) { + return tct( + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingLogsCount]]', + { + maybeWarning, + topEvents, + maybeTooltip, + matchingLogsCount, + } + ); + } + + return tct('[maybeWarning]Estimated from [maybeTooltip:[matchingLogsCount]]', { + maybeWarning, + maybeTooltip, + matchingLogsCount, + }); + } + + // otherwise, a user query was specified + // with a user query, it means we should tell the user how many of the scanned logs + // matched the user query + + // partial scans means that we didnt scan all the data so it's useful + // to mention the total number of logs available + if (dataScanned === 'partial') { + const matchingLogsCount = + sampleCount > 1 + ? t('%s matches', ) + : t('%s match', ); + + const scannedLogsCount = rawLogCounts.normal.count ? ( + rawLogCounts.normal.count > 1 ? ( + t('%s samples', ) + ) : ( + t('%s sample', ) + ) + ) : ( + + ); + + const totalLogsCount = rawLogCounts.highAccuracy.count ? ( + rawLogCounts.highAccuracy.count > 1 ? ( + t('%s logs', ) + ) : ( + t('%s log', ) + ) + ) : ( + + ); + + if (isTopN) { + return tct( + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingLogsCount]] after scanning [scannedLogsCount] of [totalLogsCount]', + { + maybeWarning, + topEvents, + maybeTooltip, + matchingLogsCount, + scannedLogsCount, + totalLogsCount, + } + ); + } + + return tct( + '[maybeWarning]Estimated from [maybeTooltip:[matchingLogsCount]] after scanning [scannedLogsCount] of [totalLogsCount]', + { + maybeWarning, + maybeTooltip, + matchingLogsCount, + scannedLogsCount, + totalLogsCount, + } + ); + } + + // otherwise, a full scan was done + // full scan means we scanned all the data available so no need to repeat that information twice + + const matchingLogsCount = + sampleCount > 1 + ? t('%s matches', ) + : t('%s match', ); + + const totalLogsCount = rawLogCounts.highAccuracy.count ? ( + rawLogCounts.highAccuracy.count > 1 ? ( + t('%s logs', ) + ) : ( + t('%s log', ) + ) + ) : ( + + ); if (isTopN) { return tct( - '[warning] Extrapolated from [matchingLogsCount] for top [topEvents] groups after scanning [tooltip:[downsampledLogsCount] of [allLogsCount]]', + '[maybeWarning]Estimated for top [topEvents] groups from [maybeTooltip:[matchingLogsCount]] of [totalLogsCount]', { - warning: , + maybeWarning, topEvents, + maybeTooltip, matchingLogsCount, - downsampledLogsCount, - allLogsCount, - tooltip: downsampledTooltip, + totalLogsCount, } ); } return tct( - '[warning] Extrapolated from [matchingLogsCount] after scanning [tooltip:[downsampledLogsCount] of [allLogsCount]]', + '[maybeWarning]Estimated from [maybeTooltip:[matchingLogsCount]] of [totalLogsCount]', { - warning: , + maybeWarning, + maybeTooltip, matchingLogsCount, - downsampledLogsCount, - allLogsCount, - tooltip: downsampledTooltip, + totalLogsCount, } ); } @@ -168,6 +307,7 @@ function DownsampledTooltip({ 'The volume of logs in this time range is too large for us to do a full scan.' )}
+
{t( 'Try reducing the date range or number of projects to attempt scanning all logs.' )} From e13e506a5e265d097a4f3f4da1b04092493f90f3 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Thu, 20 Nov 2025 11:32:01 -0500 Subject: [PATCH 5/5] update copy for the 100% sampling case --- .../explore/logs/confidenceFooter.spec.tsx | 8 +++--- .../views/explore/logs/confidenceFooter.tsx | 28 +++++++++---------- .../spans/charts/confidenceFooter.spec.tsx | 16 +++++------ .../explore/spans/charts/confidenceFooter.tsx | 28 +++++++++---------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/static/app/views/explore/logs/confidenceFooter.spec.tsx b/static/app/views/explore/logs/confidenceFooter.spec.tsx index bef06722f6fd6f..0226941ad6fe11 100644 --- a/static/app/views/explore/logs/confidenceFooter.spec.tsx +++ b/static/app/views/explore/logs/confidenceFooter.spec.tsx @@ -59,7 +59,7 @@ describe('ConfidenceFooter', () => { wrapper: Wrapper, } ); - expect(screen.getByTestId('wrapper')).toHaveTextContent('Log count: 100'); + expect(screen.getByTestId('wrapper')).toHaveTextContent('100 logs'); }); it('loaded with top events', () => { @@ -75,7 +75,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Log count for top 5 groups: 100' + '100 logs for top 5 groups' ); }); }); @@ -94,7 +94,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Log count: 100 matches of 1k logs' + '100 matches of 1k logs' ); }); @@ -111,7 +111,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Log count for top 5 groups: 100 matches of 1k logs' + '100 matches of 1k logs for top 5 groups' ); }); }); diff --git a/static/app/views/explore/logs/confidenceFooter.tsx b/static/app/views/explore/logs/confidenceFooter.tsx index 87fdba8b058579..e02da34d464467 100644 --- a/static/app/views/explore/logs/confidenceFooter.tsx +++ b/static/app/views/explore/logs/confidenceFooter.tsx @@ -71,16 +71,19 @@ function ConfidenceMessage({ // No sampling happened, so don't mention estimations. if (noSampling) { if (!hasUserQuery) { + const matchingLogsCount = + sampleCount > 1 + ? t('%s logs', ) + : t('%s log', ); + if (isTopN) { - return tct('Log count for top [topEvents] groups: [matchingLogsCount]', { + return tct('[matchingLogsCount] for top [topEvents] groups', { + matchingLogsCount, topEvents, - matchingLogsCount: , }); } - return tct('Log count: [matchingLogsCount]', { - matchingLogsCount: , - }); + return matchingLogsCount; } const matchingLogsCount = @@ -99,17 +102,14 @@ function ConfidenceMessage({ ); if (isTopN) { - return tct( - 'Log count for top [topEvents] groups: [matchingLogsCount] of [totalLogsCount]', - { - topEvents, - matchingLogsCount, - totalLogsCount, - } - ); + return tct('[matchingLogsCount] of [totalLogsCount] for top [topEvents] groups', { + matchingLogsCount, + totalLogsCount, + topEvents, + }); } - return tct('Log count: [matchingLogsCount] of [totalLogsCount]', { + return tct('[matchingLogsCount] of [totalLogsCount]', { matchingLogsCount, totalLogsCount, }); diff --git a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx index 67cdf28c887530..94e620333ec9d5 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.spec.tsx @@ -163,7 +163,7 @@ describe('ConfidenceFooter', () => { wrapper: Wrapper, } ); - expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); + expect(screen.getByTestId('wrapper')).toHaveTextContent('100 spans'); }); it('loaded with top events', () => { @@ -179,7 +179,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100' + '100 spans for top 5 groups' ); }); }); @@ -198,7 +198,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count: 100 matches of 1k spans' + '100 matches of 1k spans' ); }); @@ -216,7 +216,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100 matches of 1k spans' + '100 matches of 1k spans for top 5 groups' ); }); }); @@ -235,7 +235,7 @@ describe('ConfidenceFooter', () => { wrapper: Wrapper, } ); - expect(screen.getByTestId('wrapper')).toHaveTextContent('Span count: 100'); + expect(screen.getByTestId('wrapper')).toHaveTextContent('100 spans'); }); it('loaded with top events', () => { @@ -251,7 +251,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100' + '100 spans for top 5 groups' ); }); }); @@ -270,7 +270,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count: 100 matches of 1k spans' + '100 matches of 1k spans' ); }); @@ -288,7 +288,7 @@ describe('ConfidenceFooter', () => { } ); expect(screen.getByTestId('wrapper')).toHaveTextContent( - 'Span count for top 5 groups: 100 matches of 1k spans' + '100 matches of 1k spans for top 5 groups' ); }); }); diff --git a/static/app/views/explore/spans/charts/confidenceFooter.tsx b/static/app/views/explore/spans/charts/confidenceFooter.tsx index bc6186e58ed69f..e32d1531dee45c 100644 --- a/static/app/views/explore/spans/charts/confidenceFooter.tsx +++ b/static/app/views/explore/spans/charts/confidenceFooter.tsx @@ -91,16 +91,19 @@ function confidenceMessage({ noSampling ) { if (!userQuery) { + const matchingSpansCount = + sampleCount > 1 + ? t('%s spans', ) + : t('%s span', ); + if (isTopN) { - return tct('Span count for top [topEvents] groups: [matchingSpansCount]', { + return tct('[matchingSpansCount] for top [topEvents] groups', { topEvents, - matchingSpansCount: , + matchingSpansCount, }); } - return tct('Span count: [matchingSpansCount]', { - matchingSpansCount: , - }); + return matchingSpansCount; } const matchingSpansCount = @@ -119,17 +122,14 @@ function confidenceMessage({ ); if (isTopN) { - return tct( - 'Span count for top [topEvents] groups: [matchingSpansCount] of [totalSpansCount]', - { - topEvents, - matchingSpansCount, - totalSpansCount, - } - ); + return tct('[matchingSpansCount] of [totalSpansCount] for top [topEvents] groups', { + matchingSpansCount, + totalSpansCount, + topEvents, + }); } - return tct('Span count: [matchingSpansCount] of [totalSpansCount]', { + return tct('[matchingSpansCount] of [totalSpansCount]', { matchingSpansCount, totalSpansCount, });