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)
);
}