diff --git a/static/app/components/droppedData/band/bucketTooltip.spec.tsx b/static/app/components/droppedData/band/bucketTooltip.spec.tsx index ffb35f3343b7..451e42e92afc 100644 --- a/static/app/components/droppedData/band/bucketTooltip.spec.tsx +++ b/static/app/components/droppedData/band/bucketTooltip.spec.tsx @@ -85,6 +85,21 @@ describe('DroppedDataTooltip', () => { expect(screen.getByText('/10')).toBeInTheDocument(); }); + it('shows 0% and the time range for a bucket without drops', () => { + render( + + ); + + expect(screen.getByText('Total Dropped')).toBeInTheDocument(); + expect(screen.getByText('0%')).toBeInTheDocument(); + expect(screen.getByText('Jan 12, 2024 3:00 PM - 3:05 PM UTC')).toBeInTheDocument(); + }); + it('labels an unintentional client discard as an SDK drop', () => { render( - - - {bucket.byOutcome.map(outcome => ( - - ))} - + {bucket.byOutcome.length > 0 && ( + + + + {bucket.byOutcome.map(outcome => ( + + ))} + + + )} diff --git a/static/app/components/droppedData/band/useDroppedDataBand.spec.tsx b/static/app/components/droppedData/band/useDroppedDataBand.spec.tsx index 2521d2aef1b2..3ff177b47657 100644 --- a/static/app/components/droppedData/band/useDroppedDataBand.spec.tsx +++ b/static/app/components/droppedData/band/useDroppedDataBand.spec.tsx @@ -71,7 +71,7 @@ describe('useDroppedDataBand', () => { expect(result.current.droppedDataSeries).toBeNull(); }); - it('keeps buckets with any drops and skips those without', () => { + it('keeps buckets without drops so they can be hovered', () => { const {result} = renderHookWithProviders(() => useDroppedDataBand({ chartRef, @@ -83,13 +83,16 @@ describe('useDroppedDataBand', () => { acceptedEvents: [ DroppedEventFixture({start: 0, count: 100}), DroppedEventFixture({start: 60_000, count: 99}), + DroppedEventFixture({start: 120_000, count: 50}), ], }, }) ); expect(result.current.droppedDataSeries?.data).toEqual([ + expect.objectContaining({start: 0, ratio: 0}), expect.objectContaining({start: 60_000, ratio: 0.01}), + expect.objectContaining({start: 120_000, ratio: 0, accepted: {count: 50}}), ]); }); @@ -108,6 +111,21 @@ describe('useDroppedDataBand', () => { expect(result.current.droppedDataBandHeight).toBe(0); }); + it('hides the band when every bucket is below the highlight threshold', () => { + const {result} = renderHookWithProviders(() => + useDroppedDataBand({ + chartRef, + droppedData: { + droppedEvents: [DroppedEventFixture({start: 0, count: 1})], + acceptedEvents: [DroppedEventFixture({start: 0, count: 1999})], + }, + }) + ); + + expect(result.current.droppedDataSeries).toBeNull(); + expect(result.current.droppedDataBandHeight).toBe(0); + }); + it('draws no pill for buckets that only have accepted volume', () => { const {result} = renderHookWithProviders(() => useDroppedDataBand({ @@ -264,6 +282,18 @@ describe('useDroppedDataBand', () => { expect(gradientRect(shapes).rect.silent).toBe(true); }); + it('keeps buckets below the highlight threshold hoverable but out of the gradient', () => { + const [first, second] = renderShapes( + [bucket(3), bucket(4, 1)], + [bucket(3, 90), bucket(4, 1999)] + ); + const color = severityColor(0.1, ThemeFixture()); + const {colors} = gradientRect(first); + + expect(span(second!.at(-1)!)).toEqual([210, 270]); + expect(colors).toEqual([withAlpha(color, 0), color, color, withAlpha(color, 0)]); + }); + it('blends adjacent buckets evenly around their shared edge', () => { const theme = ThemeFixture(); const [first] = renderShapes( diff --git a/static/app/components/droppedData/band/useDroppedDataBand.tsx b/static/app/components/droppedData/band/useDroppedDataBand.tsx index bcf3b8658ee0..33763b4522bd 100644 --- a/static/app/components/droppedData/band/useDroppedDataBand.tsx +++ b/static/app/components/droppedData/band/useDroppedDataBand.tsx @@ -15,7 +15,7 @@ import {useRenderToString} from '@sentry/scraps/renderToString'; import {isChartHovered} from 'sentry/components/charts/utils'; import {DroppedDataTooltip} from 'sentry/components/droppedData/band/bucketTooltip'; import {severityColor, withAlpha} from 'sentry/components/droppedData/band/severityColor'; -import {highlightedBuckets} from 'sentry/components/droppedData/buckets'; +import {groupIntoBuckets, isHighlighted} from 'sentry/components/droppedData/buckets'; import type { DroppedDataBucket, DroppedDataProps, @@ -191,6 +191,7 @@ function droppedDataRenderItem( theme: Theme ): CustomSeriesRenderItem { const trackFill = theme.tokens.background.secondary; + const highlighted = data.filter(isHighlighted); return function renderDroppedDataItem(params, api) { const bucket = data[params.dataIndex]; @@ -213,7 +214,7 @@ function droppedDataRenderItem( if (params.dataIndexInside === 0) { children.push(bandRect(track, y, track, trackFill, {silent: true, z2: -1})); - const gradient = bandGradient(data, api); + const gradient = bandGradient(highlighted, api); if (gradient) { children.push( bandRect(clampRange(gradient.range, track), y, track, gradient.fill, { @@ -296,12 +297,12 @@ export function useDroppedDataBand({ const buckets = useMemo( () => - highlightedBuckets(droppedEvents ?? [], acceptedEvents).sort( + groupIntoBuckets(droppedEvents ?? [], acceptedEvents).sort( (a, b) => a.start - b.start ), [acceptedEvents, droppedEvents] ); - const isVisible = buckets.length > 0; + const isVisible = buckets.some(isHighlighted); const renderTooltip = useCallback( (bucket: DroppedDataBucket) => diff --git a/static/app/components/droppedData/buckets.spec.tsx b/static/app/components/droppedData/buckets.spec.tsx index 64f7237c96f8..61211ae0146a 100644 --- a/static/app/components/droppedData/buckets.spec.tsx +++ b/static/app/components/droppedData/buckets.spec.tsx @@ -86,7 +86,9 @@ describe('groupIntoBuckets', () => { ['invalid', 'transaction_sampled'], ['rate_limited', 'key_quota'], ])('excludes configured %s drops with reason %s', (outcome, reason) => { - expect(groupIntoBuckets([DroppedEventFixture({outcome, reason})])).toEqual([]); + expect(groupIntoBuckets([DroppedEventFixture({outcome, reason})])).toEqual([ + expect.objectContaining({dropped: {count: 0}, events: [], ratio: 0}), + ]); }); it.each([ @@ -142,7 +144,6 @@ describe('groupIntoBuckets', () => { ] ); - expect(buckets).toHaveLength(1); expect(buckets[0]!.accepted.count).toBe(90); expect(buckets[0]!.ratio).toBe(0.1); }); @@ -155,4 +156,46 @@ describe('groupIntoBuckets', () => { expect(buckets[0]!.accepted.count).toBe(0); expect(buckets[0]!.ratio).toBe(1); }); + + it('keeps time ranges without unintentional drops as zero-ratio buckets', () => { + const buckets = groupIntoBuckets( + [ + DroppedEventFixture({start: 0, end: 60_000, count: 10}), + DroppedEventFixture({ + start: 120_000, + end: 180_000, + outcome: 'filtered', + reason: 'web-crawlers', + }), + ], + [ + DroppedEventFixture({start: 0, end: 60_000, count: 90, outcome: 'accepted'}), + DroppedEventFixture({ + start: 60_000, + end: 120_000, + count: 30, + outcome: 'accepted', + }), + DroppedEventFixture({ + start: 60_000, + end: 120_000, + count: 20, + outcome: 'accepted', + }), + ] + ); + + expect(buckets).toEqual([ + expect.objectContaining({start: 0, ratio: 0.1}), + expect.objectContaining({ + start: 60_000, + end: 120_000, + ratio: 0, + accepted: {count: 50}, + dropped: {count: 0}, + byOutcome: [], + }), + expect.objectContaining({start: 120_000, end: 180_000, ratio: 0}), + ]); + }); }); diff --git a/static/app/components/droppedData/buckets.tsx b/static/app/components/droppedData/buckets.tsx index bc63b4433325..d1ae0f7a89c5 100644 --- a/static/app/components/droppedData/buckets.tsx +++ b/static/app/components/droppedData/buckets.tsx @@ -48,28 +48,30 @@ function addCount(volume: EventVolume, event: DroppedEventsBucket): void { volume.count += event.count; } +function getOrCreateDraft( + drafts: Map, + {start, end}: DroppedEventsBucket +): BucketDraft { + let draft = drafts.get(start); + + if (!draft) { + draft = {start, end, events: [], byOutcome: new Map(), dropped: emptyVolume()}; + drafts.set(start, draft); + } + + return draft; +} + function addDroppedEvent( - drafts: Map, + drafts: Map, event: DroppedEventsBucket ): void { + const draft = getOrCreateDraft(drafts, event); + if (isConfiguredDrop(event)) { return; } - const key = `${event.start}-${event.end}`; - let draft = drafts.get(key); - - if (!draft) { - draft = { - start: event.start, - end: event.end, - events: [], - byOutcome: new Map(), - dropped: emptyVolume(), - }; - drafts.set(key, draft); - } - draft.events.push(event); addCount(draft.dropped, event); @@ -116,15 +118,18 @@ function toBucket( } /** - * Group dropped events by their `(start, end)` time bucket, joining the - * accepted volume for the same bucket so every total has a denominator. + * Group events into one bucket per time range, joining dropped and accepted + * events so every total has a denominator. */ export function groupIntoBuckets( droppedEvents: DroppedEventsBucket[], acceptedEvents: DroppedEventsBucket[] = [] ): DroppedDataBucket[] { - const drafts = new Map(); + const drafts = new Map(); + for (const event of acceptedEvents) { + getOrCreateDraft(drafts, event); + } for (const event of droppedEvents) { addDroppedEvent(drafts, event); } @@ -134,13 +139,8 @@ export function groupIntoBuckets( return Array.from(drafts.values()).map(draft => toBucket(draft, acceptedByStart)); } -export function highlightedBuckets( - droppedEvents: DroppedEventsBucket[], - acceptedEvents?: DroppedEventsBucket[] -): DroppedDataBucket[] { - return groupIntoBuckets(droppedEvents, acceptedEvents).filter( - bucket => bucket.ratio >= MIN_HIGHLIGHT_RATIO - ); +export function isHighlighted(bucket: DroppedDataBucket): boolean { + return bucket.ratio >= MIN_HIGHLIGHT_RATIO; } export function hasDroppedData( @@ -148,6 +148,7 @@ export function hasDroppedData( acceptedEvents?: DroppedEventsBucket[] ): droppedEvents is DroppedEventsBucket[] { return ( - defined(droppedEvents) && highlightedBuckets(droppedEvents, acceptedEvents).length > 0 + defined(droppedEvents) && + groupIntoBuckets(droppedEvents, acceptedEvents).some(isHighlighted) ); }