Skip to content

Commit cd96185

Browse files
authored
feat(preprod): add compare deltas to metric cards (EME-568) (#104451)
1 parent b2cad78 commit cd96185

File tree

7 files changed

+185
-41
lines changed

7 files changed

+185
-41
lines changed

static/app/views/preprod/buildComparison/main/buildComparisonMetricCards.tsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import {Flex, Stack} from '@sentry/scraps/layout';
44
import {Heading, Text} from '@sentry/scraps/text';
55

66
import {PercentChange} from 'sentry/components/percentChange';
7-
import {IconArrow, IconCode, IconDownload} from 'sentry/icons';
7+
import {IconCode, IconDownload} from 'sentry/icons';
88
import {t} from 'sentry/locale';
99
import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10';
1010
import {MetricCard} from 'sentry/views/preprod/components/metricCard';
1111
import type {
1212
SizeAnalysisComparisonResults,
1313
SizeComparisonApiResponse,
1414
} from 'sentry/views/preprod/types/appSizeTypes';
15-
import {getLabels} from 'sentry/views/preprod/utils/labelUtils';
15+
import {getLabels, getTrend} from 'sentry/views/preprod/utils/labelUtils';
1616

1717
interface BuildComparisonMetricCardsProps {
1818
comparisonResponse: SizeComparisonApiResponse | undefined;
@@ -153,24 +153,3 @@ export function BuildComparisonMetricCards(props: BuildComparisonMetricCardsProp
153153
</Flex>
154154
);
155155
}
156-
157-
function getTrend(diff: number): {
158-
variant: 'danger' | 'success' | 'muted';
159-
icon?: React.ReactNode;
160-
} {
161-
if (diff > 0) {
162-
return {
163-
variant: 'danger',
164-
icon: <IconArrow direction="up" size="xs" />,
165-
};
166-
}
167-
168-
if (diff < 0) {
169-
return {
170-
variant: 'success',
171-
icon: <IconArrow direction="down" size="xs" />,
172-
};
173-
}
174-
175-
return {variant: 'muted'};
176-
}

static/app/views/preprod/buildDetails/buildDetails.spec.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ describe('BuildDetails', () => {
106106
download_size_bytes: 512000,
107107
},
108108
],
109+
base_size_metrics: [],
109110
},
110111
{
111112
vcs_info: PreprodVcsInfoFullFixture(),
@@ -181,6 +182,7 @@ describe('BuildDetails', () => {
181182
download_size_bytes: 512000,
182183
},
183184
],
185+
base_size_metrics: [],
184186
});
185187
},
186188
});
@@ -233,6 +235,7 @@ describe('BuildDetails', () => {
233235
download_size_bytes: 512000,
234236
},
235237
],
238+
base_size_metrics: [],
236239
}),
237240
});
238241

static/app/views/preprod/buildDetails/buildDetails.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export default function BuildDetails() {
161161
buildDetailsData={buildDetailsQuery.data}
162162
isBuildDetailsPending={buildDetailsQuery.isLoading}
163163
projectType={projectType}
164+
projectId={projectId}
164165
/>
165166
</BuildDetailsMain>
166167
</UrlParamBatchProvider>

static/app/views/preprod/buildDetails/main/buildDetailsMainContent.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface BuildDetailsMainContentProps {
4040
onRerunAnalysis: () => void;
4141
buildDetailsData?: BuildDetailsApiResponse | null;
4242
isBuildDetailsPending?: boolean;
43+
projectId?: string;
4344
projectType?: string | null;
4445
}
4546

@@ -51,6 +52,7 @@ export function BuildDetailsMainContent(props: BuildDetailsMainContentProps) {
5152
buildDetailsData,
5253
isBuildDetailsPending = false,
5354
projectType,
55+
projectId,
5456
} = props;
5557
const {
5658
data: appSizeData,
@@ -323,8 +325,11 @@ export function BuildDetailsMainContent(props: BuildDetailsMainContentProps) {
323325
sizeInfo={sizeInfo}
324326
processedInsights={processedInsights}
325327
totalSize={totalSize}
328+
artifactId={buildDetailsData?.id}
329+
baseArtifactId={buildDetailsData?.base_artifact_id ?? null}
326330
platform={buildDetailsData?.app_info?.platform ?? null}
327-
projectType={projectType}
331+
projectType={projectType ?? null}
332+
projectId={projectId}
328333
onOpenInsightsSidebar={openInsightsSidebar}
329334
/>
330335

static/app/views/preprod/buildDetails/main/buildDetailsMetricCards.tsx

Lines changed: 149 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type {ReactNode} from 'react';
22
import styled from '@emotion/styled';
33

4+
import {LinkButton} from '@sentry/scraps/button/linkButton';
45
import {Flex, Stack} from '@sentry/scraps/layout';
56
import {Heading, Text} from '@sentry/scraps/text';
67
import {Tooltip} from '@sentry/scraps/tooltip';
78

9+
import {PercentChange} from 'sentry/components/percentChange';
810
import {IconCode, IconDownload, IconLightning, IconSettings} from 'sentry/icons';
911
import {t} from 'sentry/locale';
1012
import {trackAnalytics} from 'sentry/utils/analytics';
@@ -27,14 +29,18 @@ import {
2729
formattedPrimaryMetricDownloadSize,
2830
formattedPrimaryMetricInstallSize,
2931
getLabels,
32+
getTrend,
3033
} from 'sentry/views/preprod/utils/labelUtils';
3134

3235
interface BuildDetailsMetricCardsProps {
3336
onOpenInsightsSidebar: () => void;
3437
processedInsights: ProcessedInsight[];
3538
sizeInfo: BuildDetailsSizeInfo | undefined;
3639
totalSize: number;
40+
artifactId?: string;
41+
baseArtifactId?: string | null;
3742
platform?: Platform | null;
43+
projectId?: string;
3844
projectType?: string | null;
3945
}
4046

@@ -43,11 +49,18 @@ interface MetricCardConfig {
4349
key: string;
4450
title: string;
4551
value: string;
52+
comparisonUrl?: string;
53+
delta?: MetricDelta;
4654
labelTooltip?: string;
4755
percentageText?: string;
4856
showInsightsButton?: boolean;
4957
watchBreakdown?: WatchBreakdown;
5058
}
59+
interface MetricDelta {
60+
baseValue: number;
61+
diff: number;
62+
percentageChange: number;
63+
}
5164

5265
interface WatchBreakdown {
5366
appValue: string;
@@ -59,8 +72,11 @@ export function BuildDetailsMetricCards(props: BuildDetailsMetricCardsProps) {
5972
sizeInfo,
6073
processedInsights,
6174
totalSize,
75+
artifactId,
76+
baseArtifactId,
6277
platform: platformProp,
6378
projectType,
79+
projectId,
6480
onOpenInsightsSidebar,
6581
} = props;
6682

@@ -77,6 +93,28 @@ export function BuildDetailsMetricCards(props: BuildDetailsMetricCardsProps) {
7793
);
7894
const installMetricValue = formattedPrimaryMetricInstallSize(sizeInfo);
7995
const downloadMetricValue = formattedPrimaryMetricDownloadSize(sizeInfo);
96+
97+
// Find matching base metrics for comparison
98+
const basePrimarySizeMetric = sizeInfo.base_size_metrics.find(
99+
metric => metric.metrics_artifact_type === MetricsArtifactType.MAIN_ARTIFACT
100+
);
101+
102+
// Calculate deltas for install and download sizes
103+
const installDelta = calculateDelta(
104+
primarySizeMetric?.install_size_bytes,
105+
basePrimarySizeMetric?.install_size_bytes
106+
);
107+
const downloadDelta = calculateDelta(
108+
primarySizeMetric?.download_size_bytes,
109+
basePrimarySizeMetric?.download_size_bytes
110+
);
111+
112+
// Build comparison URL using route params
113+
const comparisonUrl =
114+
baseArtifactId && projectId && artifactId
115+
? `/organizations/${organization.slug}/preprod/${projectId}/compare/${artifactId}/${baseArtifactId}/`
116+
: undefined;
117+
80118
const totalPotentialSavings = processedInsights.reduce(
81119
(sum, insight) => sum + (insight.totalSavings ?? 0),
82120
0
@@ -97,30 +135,35 @@ export function BuildDetailsMetricCards(props: BuildDetailsMetricCardsProps) {
97135
icon: <IconCode size="sm" />,
98136
labelTooltip: labels.installSizeDescription,
99137
value: installMetricValue,
138+
comparisonUrl,
100139
watchBreakdown: getWatchBreakdown(
101140
primarySizeMetric,
102141
watchArtifactMetric,
103142
'install_size_bytes'
104143
),
144+
delta: installDelta,
105145
},
106146
{
107147
key: 'download',
108148
title: labels.downloadSizeLabel,
109149
icon: <IconDownload size="sm" />,
110150
labelTooltip: labels.downloadSizeDescription,
111151
value: downloadMetricValue,
152+
comparisonUrl,
112153
watchBreakdown: getWatchBreakdown(
113154
primarySizeMetric,
114155
watchArtifactMetric,
115156
'download_size_bytes'
116157
),
158+
delta: downloadDelta,
117159
},
118160
{
119161
key: 'savings',
120162
title: t('Potential savings'),
121163
icon: <IconLightning size="sm" />,
122164
labelTooltip: t('Total savings from insights'),
123165
value: formatBytesBase10(totalPotentialSavings),
166+
comparisonUrl: undefined,
124167
percentageText: potentialSavingsPercentageText,
125168
showInsightsButton: totalPotentialSavings > 0,
126169
},
@@ -153,24 +196,95 @@ export function BuildDetailsMetricCards(props: BuildDetailsMetricCardsProps) {
153196
: undefined
154197
}
155198
>
156-
<Heading as="h3">
157-
{card.watchBreakdown ? (
158-
<Tooltip
159-
title={
160-
<WatchBreakdownTooltip
161-
appValue={card.watchBreakdown.appValue}
162-
watchValue={card.watchBreakdown.watchValue}
163-
/>
164-
}
165-
position="bottom"
166-
>
167-
<MetricValue $interactive>{card.value}</MetricValue>
168-
</Tooltip>
169-
) : (
170-
<MetricValue>{card.value}</MetricValue>
199+
<Stack gap="xs">
200+
<Flex align="end" gap="sm" wrap="wrap">
201+
<Heading as="h3">
202+
{card.watchBreakdown ? (
203+
<Tooltip
204+
title={
205+
<WatchBreakdownTooltip
206+
appValue={card.watchBreakdown.appValue}
207+
watchValue={card.watchBreakdown.watchValue}
208+
/>
209+
}
210+
position="bottom"
211+
>
212+
<MetricValue $interactive>{card.value}</MetricValue>
213+
</Tooltip>
214+
) : (
215+
<MetricValue>{card.value}</MetricValue>
216+
)}
217+
{card.percentageText ?? ''}
218+
</Heading>
219+
220+
{card.delta &&
221+
card.comparisonUrl &&
222+
(() => {
223+
const {variant, icon} = getTrend(card.delta.diff);
224+
225+
return (
226+
<LinkButton
227+
to={card.comparisonUrl}
228+
size="xs"
229+
priority="link"
230+
aria-label={t('Compare builds')}
231+
>
232+
<Flex align="center" gap="xs">
233+
{icon}
234+
<Text
235+
as="span"
236+
variant={variant}
237+
size="sm"
238+
style={{
239+
display: 'inline-flex',
240+
alignItems: 'center',
241+
flexWrap: 'wrap',
242+
gap: '0.25em',
243+
fontWeight: 'normal',
244+
}}
245+
>
246+
{card.delta.diff > 0 ? '+' : card.delta.diff < 0 ? '-' : ''}
247+
{formatBytesBase10(Math.abs(card.delta.diff))}
248+
{card.delta.percentageChange !== 0 && (
249+
<Text
250+
as="span"
251+
variant={variant}
252+
style={{
253+
display: 'inline-flex',
254+
alignItems: 'center',
255+
whiteSpace: 'nowrap',
256+
}}
257+
>
258+
{' ('}
259+
<PercentChange
260+
value={card.delta.percentageChange}
261+
minimumValue={0.001}
262+
preferredPolarity="-"
263+
colorize
264+
/>
265+
{')'}
266+
</Text>
267+
)}
268+
</Text>
269+
</Flex>
270+
</LinkButton>
271+
);
272+
})()}
273+
</Flex>
274+
275+
{card.delta && (
276+
<Flex gap="xs" wrap="wrap">
277+
<Text variant="muted" size="sm">
278+
{t('Base Build Size:')}
279+
</Text>
280+
<Text variant="muted" size="sm" bold>
281+
{card.delta.baseValue === 0
282+
? t('Not present')
283+
: formatBytesBase10(card.delta.baseValue)}
284+
</Text>
285+
</Flex>
171286
)}
172-
{card.percentageText ?? ''}
173-
</Heading>
287+
</Stack>
174288
</MetricCard>
175289
))}
176290
</Flex>
@@ -213,6 +327,24 @@ function getWatchBreakdown(
213327
};
214328
}
215329

330+
function calculateDelta(
331+
headValue: number | undefined,
332+
baseValue: number | undefined
333+
): MetricDelta | undefined {
334+
if (headValue === undefined || baseValue === undefined) {
335+
return undefined;
336+
}
337+
338+
const diff = headValue - baseValue;
339+
const percentageChange = baseValue === 0 ? 0 : diff / baseValue;
340+
341+
return {
342+
baseValue,
343+
diff,
344+
percentageChange,
345+
};
346+
}
347+
216348
const MetricValue = styled('span')<{$interactive?: boolean}>`
217349
${p =>
218350
p.$interactive

static/app/views/preprod/types/buildDetailsTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface BuildDetailsApiResponse {
99
state: BuildDetailsState;
1010
vcs_info: BuildDetailsVcsInfo;
1111
size_info?: BuildDetailsSizeInfo;
12+
base_artifact_id?: string | null;
1213
}
1314

1415
export interface BuildDetailsAppInfo {
@@ -62,6 +63,7 @@ interface BuildDetailsSizeInfoProcessing {
6263
interface BuildDetailsSizeInfoCompleted {
6364
state: BuildDetailsSizeAnalysisState.COMPLETED;
6465
size_metrics: BuildDetailsSizeInfoSizeMetric[];
66+
base_size_metrics: BuildDetailsSizeInfoSizeMetric[];
6567
}
6668

6769
interface BuildDetailsSizeInfoFailed {

0 commit comments

Comments
 (0)