Skip to content

Commit 05988cd

Browse files
authored
feat(preprod): Add size monitor UI (#108211)
Add list, detail, and create/edit views for the preprod_size_analysis detector type ("Mobile Builds"). Users can configure a metric (install/download size), measurement (absolute/diff/relative), optional build filters, and priority thresholds. Includes a live preview section, detector type selection form, and routes at /detectors/mobile-builds/. PRs: - #108208 Add size_analysis detector - #108209 Hook size analysis detector to diff - #108210 Add new issue type to frontend - #108211 Add size monitor UI (this PR) [Design doc](https://www.notion.so/sentry/Size-Monitors-3068b10e4b5d805ca57de084d1b4eba6)
1 parent 69ee9a0 commit 05988cd

20 files changed

Lines changed: 1044 additions & 40 deletions

File tree

static/app/data/platformCategories.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export const mobile: PlatformKey[] = [
5959
'cocoa-swift',
6060
];
6161

62+
export const android: PlatformKey[] = ['android', 'java-android'];
63+
64+
export const apple: PlatformKey[] = [
65+
'apple-ios',
66+
'apple-macos',
67+
'cocoa-objc',
68+
'cocoa-swift',
69+
];
70+
6271
// Mirrors `BACKEND` in src/sentry/utils/platform_categories.py
6372
// When changing this file, make sure to keep src/sentry/utils/platform_categories.py in sync.
6473
export const backend: PlatformKey[] = [

static/app/router/routes.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,10 @@ function buildRoutes(): RouteObject[] {
16611661
path: 'uptime/',
16621662
component: make(() => import('sentry/views/detectors/list/uptime')),
16631663
},
1664+
{
1665+
path: 'mobile-builds/',
1666+
component: make(() => import('sentry/views/detectors/list/mobileBuild')),
1667+
},
16641668
],
16651669
};
16661670

static/app/types/workflowEngine/detectors.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import type {
1616
UptimeMonitorMode,
1717
} from 'sentry/views/alerts/rules/uptime/types';
1818
import type {Monitor, MonitorConfig} from 'sentry/views/insights/crons/types';
19+
import type {
20+
MetricType as PreprodMeasurement,
21+
MeasurementType as PreprodThresholdType,
22+
} from 'sentry/views/settings/project/preprod/types';
23+
24+
export type {PreprodMeasurement, PreprodThresholdType};
1925

2026
/**
2127
* See SnubaQuerySerializer
@@ -84,7 +90,8 @@ export type DetectorType =
8490
| 'metric_issue'
8591
| 'monitor_check_in_failure'
8692
| 'uptime_domain_failure'
87-
| 'issue_stream';
93+
| 'issue_stream'
94+
| 'preprod_size_analysis';
8895

8996
/**
9097
* Configuration for static/threshold-based detection
@@ -165,12 +172,27 @@ export interface IssueStreamDetector extends BaseDetector {
165172
readonly type: 'issue_stream';
166173
}
167174

175+
/**
176+
* Configuration for preprod/mobile builds detection
177+
*/
178+
interface PreprodDetectorConfig {
179+
measurement: PreprodMeasurement;
180+
thresholdType: PreprodThresholdType;
181+
}
182+
183+
export interface PreprodDetector extends BaseDetector {
184+
readonly conditionGroup: MetricConditionGroup | null;
185+
readonly config: PreprodDetectorConfig;
186+
readonly type: 'preprod_size_analysis';
187+
}
188+
168189
export type Detector =
169190
| MetricDetector
170191
| UptimeDetector
171192
| CronDetector
172193
| ErrorDetector
173-
| IssueStreamDetector;
194+
| IssueStreamDetector
195+
| PreprodDetector;
174196

175197
interface UpdateConditionGroupPayload {
176198
conditions: Array<Omit<MetricCondition, 'id'>>;
@@ -230,6 +252,15 @@ export interface CronDetectorUpdatePayload extends BaseDetectorUpdatePayload {
230252
type: 'monitor_check_in_failure';
231253
}
232254

255+
export interface PreprodDetectorUpdatePayload extends BaseDetectorUpdatePayload {
256+
conditionGroup: UpdateConditionGroupPayload;
257+
config: {
258+
measurement: PreprodMeasurement;
259+
thresholdType: PreprodThresholdType;
260+
};
261+
type: 'preprod_size_analysis';
262+
}
263+
233264
export interface MetricConditionGroup {
234265
conditions: MetricCondition[];
235266
id: string;

static/app/views/detectors/components/details/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {CronDetectorDetails} from 'sentry/views/detectors/components/details/cro
99
import {ErrorDetectorDetails} from 'sentry/views/detectors/components/details/error';
1010
import {FallbackDetectorDetails} from 'sentry/views/detectors/components/details/fallback';
1111
import {MetricDetectorDetails} from 'sentry/views/detectors/components/details/metric';
12+
import {MobileBuildDetectorDetails} from 'sentry/views/detectors/components/details/mobileBuild';
1213
import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime';
1314

1415
type DetectorDetailsContentProps = {
@@ -48,6 +49,12 @@ export function DetectorDetailsContent({detector, project}: DetectorDetailsConte
4849
</Alert>
4950
</Alert.Container>
5051
);
52+
case 'preprod_size_analysis':
53+
return (
54+
<PageFiltersContainer>
55+
<MobileBuildDetectorDetails detector={detector} project={project} />
56+
</PageFiltersContainer>
57+
);
5158
default:
5259
unreachable(detectorType);
5360
return (
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import {Fragment} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {Flex, Grid} from '@sentry/scraps/layout';
5+
import {Heading, Text} from '@sentry/scraps/text';
6+
7+
import {FilterWrapper} from 'sentry/components/searchQueryBuilder/formattedQuery';
8+
import {Container} from 'sentry/components/workflowEngine/ui/container';
9+
import {t} from 'sentry/locale';
10+
import {
11+
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
12+
DetectorPriorityLevel,
13+
} from 'sentry/types/workflowEngine/dataConditions';
14+
import type {
15+
MetricCondition,
16+
PreprodDetector,
17+
} from 'sentry/types/workflowEngine/detectors';
18+
import {PriorityDot} from 'sentry/views/detectors/components/priorityDot';
19+
import {
20+
bytesToMB,
21+
getDisplayUnit,
22+
getMeasurementLabel,
23+
getMetricLabel,
24+
} from 'sentry/views/settings/project/preprod/types';
25+
26+
function getConditionLabel({condition}: {condition: MetricCondition}) {
27+
switch (condition.conditionResult) {
28+
case DetectorPriorityLevel.OK:
29+
return t('Resolved');
30+
case DetectorPriorityLevel.LOW:
31+
return t('Low');
32+
case DetectorPriorityLevel.MEDIUM:
33+
return t('Medium');
34+
case DetectorPriorityLevel.HIGH:
35+
return t('High');
36+
default:
37+
return t('Unknown');
38+
}
39+
}
40+
41+
function formatComparisonValue(
42+
comparison: MetricCondition['comparison'],
43+
thresholdType: PreprodDetector['config']['thresholdType']
44+
): string {
45+
if (typeof comparison !== 'number') {
46+
return '';
47+
}
48+
if (thresholdType === 'relative_diff') {
49+
return String(comparison);
50+
}
51+
return String(bytesToMB(comparison));
52+
}
53+
54+
function getConditionDescription({
55+
condition,
56+
thresholdType,
57+
}: {
58+
condition: MetricCondition;
59+
thresholdType: PreprodDetector['config']['thresholdType'];
60+
}) {
61+
const comparisonValue = formatComparisonValue(condition.comparison, thresholdType);
62+
const unit = getDisplayUnit(thresholdType);
63+
64+
if (condition.conditionResult === DetectorPriorityLevel.OK) {
65+
return t('Below or equal to %(value)s%(unit)s', {
66+
value: comparisonValue,
67+
unit,
68+
});
69+
}
70+
71+
return t('Above %(value)s%(unit)s', {
72+
value: comparisonValue,
73+
unit,
74+
});
75+
}
76+
77+
function DetectorPriorities({detector}: {detector: PreprodDetector}) {
78+
const conditions = detector.conditionGroup?.conditions || [];
79+
80+
return (
81+
<Grid columns="auto 1fr" gap="sm lg" align="start">
82+
{conditions.map((condition, index) => (
83+
<Fragment key={index}>
84+
<Flex align="center" gap="sm">
85+
<PriorityDot
86+
priority={
87+
condition.conditionResult === DetectorPriorityLevel.OK
88+
? 'resolved'
89+
: DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL[
90+
condition.conditionResult as keyof typeof DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL
91+
]
92+
}
93+
/>
94+
<Text>{getConditionLabel({condition})}</Text>
95+
</Flex>
96+
<Text>
97+
{getConditionDescription({
98+
condition,
99+
thresholdType: detector.config.thresholdType,
100+
})}
101+
</Text>
102+
</Fragment>
103+
))}
104+
</Grid>
105+
);
106+
}
107+
108+
export function MobileBuildDetectorDetailsDetect({
109+
detector,
110+
}: {
111+
detector: PreprodDetector;
112+
}) {
113+
const filters: Array<{key: string; value: string}> = [];
114+
115+
return (
116+
<Container>
117+
<Flex direction="column" gap="md">
118+
<Flex gap="xs" align="baseline">
119+
<Heading as="h4">{t('Measurement:')}</Heading>
120+
<Value>{getMetricLabel(detector.config.measurement)}</Value>
121+
</Flex>
122+
<Flex gap="xs" align="baseline">
123+
<Heading as="h4">{t('Threshold Type:')}</Heading>
124+
<Value>{getMeasurementLabel(detector.config.thresholdType)}</Value>
125+
</Flex>
126+
{filters.length > 0 && (
127+
<Fragment>
128+
<Heading as="h4">{t('Filters:')}</Heading>
129+
<Query>
130+
{filters.map((filter, index) => (
131+
<Fragment key={index}>
132+
<Label>
133+
<Text variant="muted">{filter.key}</Text>
134+
</Label>
135+
<Value>
136+
<Flex>
137+
<FilterWrapper>{filter.value}</FilterWrapper>
138+
</Flex>
139+
</Value>
140+
</Fragment>
141+
))}
142+
</Query>
143+
</Fragment>
144+
)}
145+
<Flex gap="xs" align="baseline">
146+
<Heading as="h4">{t('Threshold:')}</Heading>
147+
<Value>{t('Static threshold')}</Value>
148+
</Flex>
149+
<DetectorPriorities detector={detector} />
150+
</Flex>
151+
</Container>
152+
);
153+
}
154+
155+
const Query = styled('dl')`
156+
display: grid;
157+
grid-template-columns: auto minmax(0, 1fr);
158+
gap: ${p => p.theme.space.sm} ${p => p.theme.space.xs};
159+
margin: 0;
160+
align-items: baseline;
161+
`;
162+
163+
const Label = styled('dt')`
164+
color: ${p => p.theme.tokens.content.secondary};
165+
justify-self: flex-end;
166+
margin: 0;
167+
font-weight: ${p => p.theme.font.weight.sans.regular};
168+
`;
169+
170+
const Value = styled('dl')`
171+
word-break: break-all;
172+
margin: 0;
173+
`;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ErrorBoundary from 'sentry/components/errorBoundary';
2+
import DetailLayout from 'sentry/components/workflowEngine/layout/detail';
3+
import {t} from 'sentry/locale';
4+
import type {Project} from 'sentry/types/project';
5+
import type {PreprodDetector} from 'sentry/types/workflowEngine/detectors';
6+
import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations';
7+
import {DisabledAlert} from 'sentry/views/detectors/components/details/common/disabledAlert';
8+
import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
9+
import {DetectorDetailsOpenPeriodIssues} from 'sentry/views/detectors/components/details/common/openPeriodIssues';
10+
import {MobileBuildDetectorDetailsSidebar} from 'sentry/views/detectors/components/details/mobileBuild/sidebar';
11+
12+
type MobileBuildDetectorDetailsProps = {
13+
detector: PreprodDetector;
14+
project: Project;
15+
};
16+
17+
export function MobileBuildDetectorDetails({
18+
detector,
19+
project,
20+
}: MobileBuildDetectorDetailsProps) {
21+
return (
22+
<DetailLayout>
23+
<DetectorDetailsHeader detector={detector} project={project} />
24+
<DetailLayout.Body>
25+
<DetailLayout.Main>
26+
<DisabledAlert
27+
detector={detector}
28+
message={t('This monitor is disabled and not creating issues.')}
29+
/>
30+
<ErrorBoundary mini>
31+
<DetectorDetailsOpenPeriodIssues detector={detector} />
32+
</ErrorBoundary>
33+
<DetectorDetailsAutomations detector={detector} />
34+
</DetailLayout.Main>
35+
<DetailLayout.Sidebar>
36+
<MobileBuildDetectorDetailsSidebar detector={detector} />
37+
</DetailLayout.Sidebar>
38+
</DetailLayout.Body>
39+
</DetailLayout>
40+
);
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {Fragment} from 'react';
2+
3+
import ErrorBoundary from 'sentry/components/errorBoundary';
4+
import Section from 'sentry/components/workflowEngine/ui/section';
5+
import {t} from 'sentry/locale';
6+
import type {PreprodDetector} from 'sentry/types/workflowEngine/detectors';
7+
import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
8+
import {DetectorDetailsDescription} from 'sentry/views/detectors/components/details/common/description';
9+
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
10+
import {MobileBuildDetectorDetailsDetect} from 'sentry/views/detectors/components/details/mobileBuild/detect';
11+
12+
interface MobileBuildDetectorDetailsSidebarProps {
13+
detector: PreprodDetector;
14+
}
15+
16+
export function MobileBuildDetectorDetailsSidebar({
17+
detector,
18+
}: MobileBuildDetectorDetailsSidebarProps) {
19+
return (
20+
<Fragment>
21+
<Section title={t('Detect')}>
22+
<ErrorBoundary mini>
23+
<MobileBuildDetectorDetailsDetect detector={detector} />
24+
</ErrorBoundary>
25+
</Section>
26+
<DetectorDetailsAssignee owner={detector.owner} />
27+
<DetectorDetailsDescription description={detector.description} />
28+
<DetectorExtraDetails>
29+
<DetectorExtraDetails.DateCreated detector={detector} />
30+
<DetectorExtraDetails.CreatedBy detector={detector} />
31+
<DetectorExtraDetails.LastModified detector={detector} />
32+
<DetectorExtraDetails.Environment detector={detector} />
33+
</DetectorExtraDetails>
34+
</Fragment>
35+
);
36+
}

0 commit comments

Comments
 (0)