Skip to content

ref(aci): Fully separate detector types #95182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 9, 2025
Merged
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
55 changes: 34 additions & 21 deletions static/app/types/workflowEngine/detectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ interface SnubaQuery {
environment?: string;
}

/**
* See DataSourceSerializer
*/
interface BaseDataSource {
id: string;
organizationId: string;
Expand Down Expand Up @@ -59,17 +62,10 @@ interface UptimeSubscriptionDataSource extends BaseDataSource {
timeoutMs: number;
traceSampling: boolean;
url: string;
urlDomain: string;
urlDomainSuffix: string;
};
type: 'uptime_subscription';
}

/**
* See DataSourceSerializer
*/
export type DataSource = SnubaQueryDataSource | UptimeSubscriptionDataSource;

export type DetectorType =
| 'error'
| 'metric_issue'
Expand Down Expand Up @@ -114,28 +110,45 @@ interface UptimeDetectorConfig {
environment: string;
}

export type DetectorConfig = MetricDetectorConfig | UptimeDetectorConfig;

interface NewDetector {
conditionGroup: DataConditionGroup | null;
config: DetectorConfig;
dataSources: DataSource[] | null;
type BaseDetector = Readonly<{
createdBy: string | null;
dateCreated: string;
dateUpdated: string;
disabled: boolean;
id: string;
lastTriggered: string;
name: string;
owner: string | null;
projectId: string;
type: DetectorType;
workflowIds: string[];
}>;

export interface MetricDetector extends BaseDetector {
readonly conditionGroup: DataConditionGroup | null;
readonly config: MetricDetectorConfig;
readonly dataSources: SnubaQueryDataSource[];
readonly type: 'metric_issue';
}

export interface UptimeDetector extends BaseDetector {
readonly config: UptimeDetectorConfig;
readonly dataSources: UptimeSubscriptionDataSource[];
readonly type: 'uptime_domain_failure';
}

export interface Detector extends Readonly<NewDetector> {
readonly createdBy: string | null;
readonly dateCreated: string;
readonly dateUpdated: string;
readonly id: string;
readonly lastTriggered: string;
readonly owner: string | null;
interface CronDetector extends BaseDetector {
// TODO: Add cron detector type fields
readonly type: 'uptime_subscription';
}

export interface ErrorDetector extends BaseDetector {
// TODO: Add error detector type fields
readonly type: 'error';
}

export type Detector = MetricDetector | UptimeDetector | CronDetector | ErrorDetector;

interface UpdateConditionGroupPayload {
conditions: Array<Omit<DataCondition, 'id'>>;
logicType: DataConditionGroup['logicType'];
Expand Down Expand Up @@ -172,7 +185,7 @@ export interface UptimeDetectorUpdatePayload extends BaseDetectorUpdatePayload {

export interface MetricDetectorUpdatePayload extends BaseDetectorUpdatePayload {
conditionGroup: UpdateConditionGroupPayload;
config: DetectorConfig;
config: MetricDetectorConfig;
dataSource: UpdateSnubaDataSourcePayload;
type: 'metric_issue';
}
6 changes: 3 additions & 3 deletions static/app/views/automations/list.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {AutomationFixture} from 'sentry-fixture/automations';
import {DetectorFixture} from 'sentry-fixture/detectors';
import {MetricDetectorFixture} from 'sentry-fixture/detectors';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {PageFiltersFixture} from 'sentry-fixture/pageFilters';
import {ProjectFixture} from 'sentry-fixture/project';
Expand Down Expand Up @@ -32,7 +32,7 @@ describe('AutomationsList', function () {
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/detectors/1/',
body: [DetectorFixture({name: 'Detector 1'})],
body: [MetricDetectorFixture({name: 'Detector 1'})],
});
PageFiltersStore.onInitializeUrlState(PageFiltersFixture({projects: [1]}), new Set());
});
Expand All @@ -59,7 +59,7 @@ describe('AutomationsList', function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/detectors/',
body: [
DetectorFixture({
MetricDetectorFixture({
id: '1',
name: 'Detector 1',
workflowIds: ['100'],
Expand Down
8 changes: 5 additions & 3 deletions static/app/views/detectors/components/detailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
}

function DetailsPanel({detector}: DetailsPanelProps) {
const dataSource = detector.dataSources?.[0];
if (dataSource?.type === 'snuba_query_subscription') {
return <SnubaQueryDetails dataSource={dataSource} />;
if (detector.type === 'metric_issue') {
const dataSource = detector.dataSources?.[0];
if (dataSource?.type === 'snuba_query_subscription') {
return <SnubaQueryDetails dataSource={dataSource} />;
}
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import {getResolutionDescription} from 'sentry/views/detectors/utils/getDetector
import {getMetricDetectorSuffix} from 'sentry/views/detectors/utils/metricDetectorSuffix';

function getDetectorEnvironment(detector: Detector) {
// TODO: Add support for other detector types
if (detector.type !== 'metric_issue') {
return '<placeholder>';
}

return (
detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
?.snubaQuery.environment ?? t('All environments')
Expand Down Expand Up @@ -67,6 +72,10 @@ function AssignToUser({userId}: {userId: string}) {
}

function DetectorPriorities({detector}: {detector: Detector}) {
if (detector.type !== 'metric_issue') {
return null;
}

// TODO: Add support for other detector types
if (!('detectionType' in detector.config)) {
return null;
Expand Down Expand Up @@ -126,7 +135,7 @@ function DetectorPriorities({detector}: {detector: Detector}) {

function DetectorResolve({detector}: {detector: Detector}) {
// TODO: Add support for other detector types
if (!('detectionType' in detector.config)) {
if (detector.type !== 'metric_issue') {
return null;
}

Expand Down
99 changes: 52 additions & 47 deletions static/app/views/detectors/components/detectorLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
import type {DataSource, Detector} from 'sentry/types/workflowEngine/detectors';
import type {
Detector,
MetricDetector,
UptimeDetector,
} from 'sentry/types/workflowEngine/detectors';
import {defined} from 'sentry/utils';
import getDuration from 'sentry/utils/duration/getDuration';
import {middleEllipsis} from 'sentry/utils/string/middleEllipsis';
Expand Down Expand Up @@ -76,12 +80,7 @@ function DetailItem({children}: {children: React.ReactNode}) {
);
}

function ConfigDetails({detector}: {detector: Detector}) {
// TODO: Use a MetricDetector type to avoid checking for this
if (!('detectionType' in detector.config)) {
return null;
}

function MetricDetectorConfigDetails({detector}: {detector: MetricDetector}) {
const type = detector.config.detectionType;
const conditions = detector.conditionGroup?.conditions;
if (!conditions?.length) {
Expand Down Expand Up @@ -118,54 +117,60 @@ function ConfigDetails({detector}: {detector: Detector}) {
}
}

function DataSourceDetails({dataSource}: {dataSource: DataSource}) {
const type = dataSource.type;
switch (type) {
case 'snuba_query_subscription':
if (!dataSource.queryObj) {
return <DetailItem>{t('Query not found.')}</DetailItem>;
}
return (
<Fragment>
<DetailItem>{dataSource.queryObj.snubaQuery.environment}</DetailItem>
<DetailItem>{dataSource.queryObj.snubaQuery.aggregate}</DetailItem>
<DetailItem>
{middleEllipsis(dataSource.queryObj.snubaQuery.query, 40)}
</DetailItem>
</Fragment>
);
case 'uptime_subscription':
return (
<Fragment>
<DetailItem>
{dataSource.queryObj.urlDomain + '.' + dataSource.queryObj.urlDomainSuffix}
</DetailItem>
<DetailItem>{getDuration(dataSource.queryObj.intervalSeconds)}</DetailItem>
</Fragment>
);
default:
unreachable(type);
return null;
}
function MetricDetectorDetails({detector}: {detector: MetricDetector}) {
return (
<Fragment>
{detector.dataSources.map(dataSource => {
if (!dataSource.queryObj) {
return null;
}
return (
<Fragment key={dataSource.id}>
<DetailItem>{dataSource.queryObj.snubaQuery.environment}</DetailItem>
<DetailItem>{dataSource.queryObj.snubaQuery.aggregate}</DetailItem>
<DetailItem>
{middleEllipsis(dataSource.queryObj.snubaQuery.query, 40)}
</DetailItem>
</Fragment>
);
})}
<MetricDetectorConfigDetails detector={detector} />
</Fragment>
);
}

function Details({detector}: {detector: Detector}) {
if (!detector.dataSources?.length) {
return null;
}

function UptimeDetectorDetails({detector}: {detector: UptimeDetector}) {
return (
<Fragment>
{detector.dataSources.map(dataSource => (
<Fragment key={dataSource.id}>
<DataSourceDetails dataSource={dataSource} />
</Fragment>
))}
<ConfigDetails detector={detector} />
{detector.dataSources.map(dataSource => {
return (
<Fragment key={dataSource.id}>
<DetailItem>{middleEllipsis(dataSource.queryObj.url, 40)}</DetailItem>
<DetailItem>{getDuration(dataSource.queryObj.intervalSeconds)}</DetailItem>
</Fragment>
);
})}
</Fragment>
);
}

function Details({detector}: {detector: Detector}) {
const detectorType = detector.type;
switch (detectorType) {
case 'metric_issue':
return <MetricDetectorDetails detector={detector} />;
case 'uptime_domain_failure':
return <UptimeDetectorDetails detector={detector} />;
// TODO: Implement details for Cron detectors
case 'uptime_subscription':
case 'error':
return null;
default:
unreachable(detectorType);
return null;
}
}

export function DetectorLink({detector, className}: DetectorLinkProps) {
const org = useOrganization();
const project = useProjectFromId({project_id: detector.projectId});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {DetectorFixture} from 'sentry-fixture/detectors';
import {MetricDetectorFixture} from 'sentry-fixture/detectors';
import {OrganizationFixture} from 'sentry-fixture/organization';

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

describe('EditDetectorActions', () => {
it('calls delete mutation when deletion is confirmed', async () => {
const detector = DetectorFixture();
const detector = MetricDetectorFixture();
const organization = OrganizationFixture();

const mockDeleteDetector = MockApiClient.addMockResponse({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from 'sentry/types/workflowEngine/dataConditions';
import type {
Detector,
DetectorConfig,
MetricDetector,
MetricDetectorConfig,
MetricDetectorUpdatePayload,
} from 'sentry/types/workflowEngine/detectors';
import {defined} from 'sentry/utils';
Expand Down Expand Up @@ -325,7 +326,7 @@ export function metricDetectorFormDataToEndpointPayload(
const dataSource = createDataSource(data);

// Create config based on detection type
let config: DetectorConfig;
let config: MetricDetectorConfig;
switch (data.kind) {
case 'percent':
config = {
Expand Down Expand Up @@ -368,7 +369,7 @@ export function metricDetectorFormDataToEndpointPayload(
* Convert the detector conditions array to the flattened form data
*/
function processDetectorConditions(
detector: Detector
detector: MetricDetector
): PrioritizeLevelFormData &
Pick<MetricDetectorFormData, 'conditionValue' | 'conditionType'> {
// Get conditions from the condition group
Expand Down Expand Up @@ -422,6 +423,11 @@ function processDetectorConditions(
export function metricSavedDetectorToFormData(
detector: Detector
): MetricDetectorFormData {
if (detector.type !== 'metric_issue') {
// This should never happen
throw new Error('Detector type mismatch');
}

// Get the first data source (assuming metric detectors have one)
const dataSource = detector.dataSources?.[0];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export function uptimeFormDataToEndpointPayload(
export function uptimeSavedDetectorToFormData(
detector: Detector
): UptimeDetectorFormData {
if (detector.type !== 'uptime_domain_failure') {
// This should never happen
throw new Error('Detector type mismatch');
}

const dataSource = detector.dataSources?.[0];
const environment = 'environment' in detector.config ? detector.config.environment : '';

Expand Down
7 changes: 5 additions & 2 deletions static/app/views/detectors/detail.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {AutomationFixture} from 'sentry-fixture/automations';
import {DetectorFixture, SnubaQueryDataSourceFixture} from 'sentry-fixture/detectors';
import {
MetricDetectorFixture,
SnubaQueryDataSourceFixture,
} from 'sentry-fixture/detectors';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
import {TeamFixture} from 'sentry-fixture/team';
Expand All @@ -26,7 +29,7 @@ describe('DetectorDetails', function () {
},
},
});
const snubaQueryDetector = DetectorFixture({
const snubaQueryDetector = MetricDetectorFixture({
projectId: project.id,
dataSources: [dataSource],
owner: `team:${ownerTeam.id}`,
Expand Down
Loading
Loading