Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions static/app/components/droppedData/band/bucketTooltip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ describe('DroppedDataTooltip', () => {
expect(screen.getByText('/10')).toBeInTheDocument();
});

it('shows 0% and the time range for a bucket without drops', () => {
render(
<ExampleDroppedDataTooltip
dropped={[]}
accepted={[
DroppedEventFixture({start: START, end: END, outcome: 'accepted', count: 500}),
]}
/>
);

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(
<ExampleDroppedDataTooltip
Expand Down
24 changes: 14 additions & 10 deletions static/app/components/droppedData/band/bucketTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,20 @@ export function DroppedDataTooltip({bucket, timezone}: DroppedDataTooltipProps)
{formatDroppedShare(bucket.ratio)}
</Text>
</Flex>
<Separator orientation="horizontal" />
<Stack as="section" gap="md" padding="lg xl">
{bucket.byOutcome.map(outcome => (
<VolumeRow
key={outcome.outcome}
label={outcomeLabel(outcome)}
{...formatCountRatio(outcome.count, totalEvents)}
/>
))}
</Stack>
{bucket.byOutcome.length > 0 && (
<Fragment>
<Separator orientation="horizontal" />
<Stack as="section" gap="md" padding="lg xl">
{bucket.byOutcome.map(outcome => (
<VolumeRow
key={outcome.outcome}
label={outcomeLabel(outcome)}
{...formatCountRatio(outcome.count, totalEvents)}
/>
))}
</Stack>
</Fragment>
)}
</TooltipBody>
<Container className="tooltip-footer tooltip-footer-centered">
<Text size="xs" variant="muted">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}}),
]);
});

Expand All @@ -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({
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 5 additions & 4 deletions static/app/components/droppedData/band/useDroppedDataBand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand All @@ -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, {
Expand Down Expand Up @@ -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) =>
Expand Down
47 changes: 45 additions & 2 deletions static/app/components/droppedData/buckets.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -142,7 +144,6 @@ describe('groupIntoBuckets', () => {
]
);

expect(buckets).toHaveLength(1);
expect(buckets[0]!.accepted.count).toBe(90);
expect(buckets[0]!.ratio).toBe(0.1);
});
Expand All @@ -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}),
]);
});
});
53 changes: 27 additions & 26 deletions static/app/components/droppedData/buckets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,30 @@ function addCount(volume: EventVolume, event: DroppedEventsBucket): void {
volume.count += event.count;
}

function getOrCreateDraft(
drafts: Map<number, BucketDraft>,
{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<string, BucketDraft>,
drafts: Map<number, BucketDraft>,
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);

Expand Down Expand Up @@ -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<string, BucketDraft>();
const drafts = new Map<number, BucketDraft>();

for (const event of acceptedEvents) {
getOrCreateDraft(drafts, event);
}
for (const event of droppedEvents) {
addDroppedEvent(drafts, event);
}
Expand All @@ -134,20 +139,16 @@ 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(
droppedEvents: DroppedEventsBucket[] | undefined,
acceptedEvents?: DroppedEventsBucket[]
): droppedEvents is DroppedEventsBucket[] {
return (
defined(droppedEvents) && highlightedBuckets(droppedEvents, acceptedEvents).length > 0
defined(droppedEvents) &&
groupIntoBuckets(droppedEvents, acceptedEvents).some(isHighlighted)
);
}
Loading