Skip to content

Commit e473371

Browse files
authored
ref(aci): Fully separate detector types (#95182)
We were using a shared `Detector` type everywhere, which meant that there were a lot of type guards sprinkled around. This introduces more concrete types for each kind of detector to make type checking more useful.
1 parent c7b6210 commit e473371

File tree

13 files changed

+163
-102
lines changed

13 files changed

+163
-102
lines changed

static/app/types/workflowEngine/detectors.tsx

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ interface SnubaQuery {
2525
environment?: string;
2626
}
2727

28+
/**
29+
* See DataSourceSerializer
30+
*/
2831
interface BaseDataSource {
2932
id: string;
3033
organizationId: string;
@@ -59,17 +62,10 @@ interface UptimeSubscriptionDataSource extends BaseDataSource {
5962
timeoutMs: number;
6063
traceSampling: boolean;
6164
url: string;
62-
urlDomain: string;
63-
urlDomainSuffix: string;
6465
};
6566
type: 'uptime_subscription';
6667
}
6768

68-
/**
69-
* See DataSourceSerializer
70-
*/
71-
export type DataSource = SnubaQueryDataSource | UptimeSubscriptionDataSource;
72-
7369
export type DetectorType =
7470
| 'error'
7571
| 'metric_issue'
@@ -114,28 +110,45 @@ interface UptimeDetectorConfig {
114110
environment: string;
115111
}
116112

117-
export type DetectorConfig = MetricDetectorConfig | UptimeDetectorConfig;
118-
119-
interface NewDetector {
120-
conditionGroup: DataConditionGroup | null;
121-
config: DetectorConfig;
122-
dataSources: DataSource[] | null;
113+
type BaseDetector = Readonly<{
114+
createdBy: string | null;
115+
dateCreated: string;
116+
dateUpdated: string;
123117
disabled: boolean;
118+
id: string;
119+
lastTriggered: string;
124120
name: string;
121+
owner: string | null;
125122
projectId: string;
126123
type: DetectorType;
127124
workflowIds: string[];
125+
}>;
126+
127+
export interface MetricDetector extends BaseDetector {
128+
readonly conditionGroup: DataConditionGroup | null;
129+
readonly config: MetricDetectorConfig;
130+
readonly dataSources: SnubaQueryDataSource[];
131+
readonly type: 'metric_issue';
132+
}
133+
134+
export interface UptimeDetector extends BaseDetector {
135+
readonly config: UptimeDetectorConfig;
136+
readonly dataSources: UptimeSubscriptionDataSource[];
137+
readonly type: 'uptime_domain_failure';
128138
}
129139

130-
export interface Detector extends Readonly<NewDetector> {
131-
readonly createdBy: string | null;
132-
readonly dateCreated: string;
133-
readonly dateUpdated: string;
134-
readonly id: string;
135-
readonly lastTriggered: string;
136-
readonly owner: string | null;
140+
interface CronDetector extends BaseDetector {
141+
// TODO: Add cron detector type fields
142+
readonly type: 'uptime_subscription';
137143
}
138144

145+
export interface ErrorDetector extends BaseDetector {
146+
// TODO: Add error detector type fields
147+
readonly type: 'error';
148+
}
149+
150+
export type Detector = MetricDetector | UptimeDetector | CronDetector | ErrorDetector;
151+
139152
interface UpdateConditionGroupPayload {
140153
conditions: Array<Omit<DataCondition, 'id'>>;
141154
logicType: DataConditionGroup['logicType'];
@@ -172,7 +185,7 @@ export interface UptimeDetectorUpdatePayload extends BaseDetectorUpdatePayload {
172185

173186
export interface MetricDetectorUpdatePayload extends BaseDetectorUpdatePayload {
174187
conditionGroup: UpdateConditionGroupPayload;
175-
config: DetectorConfig;
188+
config: MetricDetectorConfig;
176189
dataSource: UpdateSnubaDataSourcePayload;
177190
type: 'metric_issue';
178191
}

static/app/views/automations/list.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {AutomationFixture} from 'sentry-fixture/automations';
2-
import {DetectorFixture} from 'sentry-fixture/detectors';
2+
import {MetricDetectorFixture} from 'sentry-fixture/detectors';
33
import {OrganizationFixture} from 'sentry-fixture/organization';
44
import {PageFiltersFixture} from 'sentry-fixture/pageFilters';
55
import {ProjectFixture} from 'sentry-fixture/project';
@@ -32,7 +32,7 @@ describe('AutomationsList', function () {
3232
});
3333
MockApiClient.addMockResponse({
3434
url: '/organizations/org-slug/detectors/1/',
35-
body: [DetectorFixture({name: 'Detector 1'})],
35+
body: [MetricDetectorFixture({name: 'Detector 1'})],
3636
});
3737
PageFiltersStore.onInitializeUrlState(PageFiltersFixture({projects: [1]}), new Set());
3838
});
@@ -59,7 +59,7 @@ describe('AutomationsList', function () {
5959
MockApiClient.addMockResponse({
6060
url: '/organizations/org-slug/detectors/',
6161
body: [
62-
DetectorFixture({
62+
MetricDetectorFixture({
6363
id: '1',
6464
name: 'Detector 1',
6565
workflowIds: ['100'],

static/app/views/detectors/components/detailsPanel.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
4141
}
4242

4343
function DetailsPanel({detector}: DetailsPanelProps) {
44-
const dataSource = detector.dataSources?.[0];
45-
if (dataSource?.type === 'snuba_query_subscription') {
46-
return <SnubaQueryDetails dataSource={dataSource} />;
44+
if (detector.type === 'metric_issue') {
45+
const dataSource = detector.dataSources?.[0];
46+
if (dataSource?.type === 'snuba_query_subscription') {
47+
return <SnubaQueryDetails dataSource={dataSource} />;
48+
}
4749
}
4850

4951
return (

static/app/views/detectors/components/detectorDetailsSidebar.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import {getResolutionDescription} from 'sentry/views/detectors/utils/getDetector
2626
import {getMetricDetectorSuffix} from 'sentry/views/detectors/utils/metricDetectorSuffix';
2727

2828
function getDetectorEnvironment(detector: Detector) {
29+
// TODO: Add support for other detector types
30+
if (detector.type !== 'metric_issue') {
31+
return '<placeholder>';
32+
}
33+
2934
return (
3035
detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
3136
?.snubaQuery.environment ?? t('All environments')
@@ -67,6 +72,10 @@ function AssignToUser({userId}: {userId: string}) {
6772
}
6873

6974
function DetectorPriorities({detector}: {detector: Detector}) {
75+
if (detector.type !== 'metric_issue') {
76+
return null;
77+
}
78+
7079
// TODO: Add support for other detector types
7180
if (!('detectionType' in detector.config)) {
7281
return null;
@@ -126,7 +135,7 @@ function DetectorPriorities({detector}: {detector: Detector}) {
126135

127136
function DetectorResolve({detector}: {detector: Detector}) {
128137
// TODO: Add support for other detector types
129-
if (!('detectionType' in detector.config)) {
138+
if (detector.type !== 'metric_issue') {
130139
return null;
131140
}
132141

static/app/views/detectors/components/detectorLink.tsx

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
1212
DetectorPriorityLevel,
1313
} from 'sentry/types/workflowEngine/dataConditions';
14-
import type {DataSource, Detector} from 'sentry/types/workflowEngine/detectors';
14+
import type {
15+
Detector,
16+
MetricDetector,
17+
UptimeDetector,
18+
} from 'sentry/types/workflowEngine/detectors';
1519
import {defined} from 'sentry/utils';
1620
import getDuration from 'sentry/utils/duration/getDuration';
1721
import {middleEllipsis} from 'sentry/utils/string/middleEllipsis';
@@ -76,12 +80,7 @@ function DetailItem({children}: {children: React.ReactNode}) {
7680
);
7781
}
7882

79-
function ConfigDetails({detector}: {detector: Detector}) {
80-
// TODO: Use a MetricDetector type to avoid checking for this
81-
if (!('detectionType' in detector.config)) {
82-
return null;
83-
}
84-
83+
function MetricDetectorConfigDetails({detector}: {detector: MetricDetector}) {
8584
const type = detector.config.detectionType;
8685
const conditions = detector.conditionGroup?.conditions;
8786
if (!conditions?.length) {
@@ -118,54 +117,60 @@ function ConfigDetails({detector}: {detector: Detector}) {
118117
}
119118
}
120119

121-
function DataSourceDetails({dataSource}: {dataSource: DataSource}) {
122-
const type = dataSource.type;
123-
switch (type) {
124-
case 'snuba_query_subscription':
125-
if (!dataSource.queryObj) {
126-
return <DetailItem>{t('Query not found.')}</DetailItem>;
127-
}
128-
return (
129-
<Fragment>
130-
<DetailItem>{dataSource.queryObj.snubaQuery.environment}</DetailItem>
131-
<DetailItem>{dataSource.queryObj.snubaQuery.aggregate}</DetailItem>
132-
<DetailItem>
133-
{middleEllipsis(dataSource.queryObj.snubaQuery.query, 40)}
134-
</DetailItem>
135-
</Fragment>
136-
);
137-
case 'uptime_subscription':
138-
return (
139-
<Fragment>
140-
<DetailItem>
141-
{dataSource.queryObj.urlDomain + '.' + dataSource.queryObj.urlDomainSuffix}
142-
</DetailItem>
143-
<DetailItem>{getDuration(dataSource.queryObj.intervalSeconds)}</DetailItem>
144-
</Fragment>
145-
);
146-
default:
147-
unreachable(type);
148-
return null;
149-
}
120+
function MetricDetectorDetails({detector}: {detector: MetricDetector}) {
121+
return (
122+
<Fragment>
123+
{detector.dataSources.map(dataSource => {
124+
if (!dataSource.queryObj) {
125+
return null;
126+
}
127+
return (
128+
<Fragment key={dataSource.id}>
129+
<DetailItem>{dataSource.queryObj.snubaQuery.environment}</DetailItem>
130+
<DetailItem>{dataSource.queryObj.snubaQuery.aggregate}</DetailItem>
131+
<DetailItem>
132+
{middleEllipsis(dataSource.queryObj.snubaQuery.query, 40)}
133+
</DetailItem>
134+
</Fragment>
135+
);
136+
})}
137+
<MetricDetectorConfigDetails detector={detector} />
138+
</Fragment>
139+
);
150140
}
151141

152-
function Details({detector}: {detector: Detector}) {
153-
if (!detector.dataSources?.length) {
154-
return null;
155-
}
156-
142+
function UptimeDetectorDetails({detector}: {detector: UptimeDetector}) {
157143
return (
158144
<Fragment>
159-
{detector.dataSources.map(dataSource => (
160-
<Fragment key={dataSource.id}>
161-
<DataSourceDetails dataSource={dataSource} />
162-
</Fragment>
163-
))}
164-
<ConfigDetails detector={detector} />
145+
{detector.dataSources.map(dataSource => {
146+
return (
147+
<Fragment key={dataSource.id}>
148+
<DetailItem>{middleEllipsis(dataSource.queryObj.url, 40)}</DetailItem>
149+
<DetailItem>{getDuration(dataSource.queryObj.intervalSeconds)}</DetailItem>
150+
</Fragment>
151+
);
152+
})}
165153
</Fragment>
166154
);
167155
}
168156

157+
function Details({detector}: {detector: Detector}) {
158+
const detectorType = detector.type;
159+
switch (detectorType) {
160+
case 'metric_issue':
161+
return <MetricDetectorDetails detector={detector} />;
162+
case 'uptime_domain_failure':
163+
return <UptimeDetectorDetails detector={detector} />;
164+
// TODO: Implement details for Cron detectors
165+
case 'uptime_subscription':
166+
case 'error':
167+
return null;
168+
default:
169+
unreachable(detectorType);
170+
return null;
171+
}
172+
}
173+
169174
export function DetectorLink({detector, className}: DetectorLinkProps) {
170175
const org = useOrganization();
171176
const project = useProjectFromId({project_id: detector.projectId});

static/app/views/detectors/components/forms/editDetectorActions.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {DetectorFixture} from 'sentry-fixture/detectors';
1+
import {MetricDetectorFixture} from 'sentry-fixture/detectors';
22
import {OrganizationFixture} from 'sentry-fixture/organization';
33

44
import {
@@ -13,7 +13,7 @@ import {EditDetectorActions} from './editDetectorActions';
1313

1414
describe('EditDetectorActions', () => {
1515
it('calls delete mutation when deletion is confirmed', async () => {
16-
const detector = DetectorFixture();
16+
const detector = MetricDetectorFixture();
1717
const organization = OrganizationFixture();
1818

1919
const mockDeleteDetector = MockApiClient.addMockResponse({

static/app/views/detectors/components/forms/metric/metricFormData.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
} from 'sentry/types/workflowEngine/dataConditions';
1111
import type {
1212
Detector,
13-
DetectorConfig,
13+
MetricDetector,
14+
MetricDetectorConfig,
1415
MetricDetectorUpdatePayload,
1516
} from 'sentry/types/workflowEngine/detectors';
1617
import {defined} from 'sentry/utils';
@@ -325,7 +326,7 @@ export function metricDetectorFormDataToEndpointPayload(
325326
const dataSource = createDataSource(data);
326327

327328
// Create config based on detection type
328-
let config: DetectorConfig;
329+
let config: MetricDetectorConfig;
329330
switch (data.kind) {
330331
case 'percent':
331332
config = {
@@ -368,7 +369,7 @@ export function metricDetectorFormDataToEndpointPayload(
368369
* Convert the detector conditions array to the flattened form data
369370
*/
370371
function processDetectorConditions(
371-
detector: Detector
372+
detector: MetricDetector
372373
): PrioritizeLevelFormData &
373374
Pick<MetricDetectorFormData, 'conditionValue' | 'conditionType'> {
374375
// Get conditions from the condition group
@@ -422,6 +423,11 @@ function processDetectorConditions(
422423
export function metricSavedDetectorToFormData(
423424
detector: Detector
424425
): MetricDetectorFormData {
426+
if (detector.type !== 'metric_issue') {
427+
// This should never happen
428+
throw new Error('Detector type mismatch');
429+
}
430+
425431
// Get the first data source (assuming metric detectors have one)
426432
const dataSource = detector.dataSources?.[0];
427433

static/app/views/detectors/components/forms/uptime/fields.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export function uptimeFormDataToEndpointPayload(
7070
export function uptimeSavedDetectorToFormData(
7171
detector: Detector
7272
): UptimeDetectorFormData {
73+
if (detector.type !== 'uptime_domain_failure') {
74+
// This should never happen
75+
throw new Error('Detector type mismatch');
76+
}
77+
7378
const dataSource = detector.dataSources?.[0];
7479
const environment = 'environment' in detector.config ? detector.config.environment : '';
7580

static/app/views/detectors/detail.spec.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {AutomationFixture} from 'sentry-fixture/automations';
2-
import {DetectorFixture, SnubaQueryDataSourceFixture} from 'sentry-fixture/detectors';
2+
import {
3+
MetricDetectorFixture,
4+
SnubaQueryDataSourceFixture,
5+
} from 'sentry-fixture/detectors';
36
import {OrganizationFixture} from 'sentry-fixture/organization';
47
import {ProjectFixture} from 'sentry-fixture/project';
58
import {TeamFixture} from 'sentry-fixture/team';
@@ -26,7 +29,7 @@ describe('DetectorDetails', function () {
2629
},
2730
},
2831
});
29-
const snubaQueryDetector = DetectorFixture({
32+
const snubaQueryDetector = MetricDetectorFixture({
3033
projectId: project.id,
3134
dataSources: [dataSource],
3235
owner: `team:${ownerTeam.id}`,

0 commit comments

Comments
 (0)