diff --git a/static/app/types/integrations.tsx b/static/app/types/integrations.tsx index 5a5db6e2d439..4952d0f7d841 100644 --- a/static/app/types/integrations.tsx +++ b/static/app/types/integrations.tsx @@ -550,7 +550,13 @@ export type IntegrationIssueConfig = { export type AppOrProviderOrPlugin = SentryApp | IntegrationProvider | DocIntegration; -export type WebhookEvent = 'issue' | 'error' | 'comment' | 'seer' | 'preprod_artifact'; +export type WebhookEvent = + | 'issue' + | 'error' + | 'comment' + | 'seer' + | 'preprod_artifact' + | 'alert'; /** * Codeowners and repository path mappings. diff --git a/static/app/views/settings/organizationDeveloperSettings/constants.tsx b/static/app/views/settings/organizationDeveloperSettings/constants.tsx index 7d6dd8ac742b..90d18bf649c6 100644 --- a/static/app/views/settings/organizationDeveloperSettings/constants.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/constants.tsx @@ -7,6 +7,7 @@ export const EVENT_CHOICES = [ 'comment', 'seer', 'preprod_artifact', + 'alert', ] as const satisfies readonly WebhookEvent[]; // The webhook subscription vocabulary, mirroring EVENT_EXPANSION in @@ -36,6 +37,7 @@ export const RESOURCE_EVENTS = { 'preprod_artifact.size_analysis_completed', 'preprod_artifact.build_distribution_completed', ], + alert: ['alert.triggered'], } as const satisfies Record; export type WebhookSubscription = (typeof RESOURCE_EVENTS)[WebhookEvent][number]; @@ -46,6 +48,7 @@ export const WEBHOOK_SUBSCRIPTION_CHOICES = [ ...RESOURCE_EVENTS.comment, ...RESOURCE_EVENTS.seer, ...RESOURCE_EVENTS.preprod_artifact, + ...RESOURCE_EVENTS.alert, ] as const; const LEGACY_EVENT_ALIASES: Record = { @@ -86,6 +89,7 @@ const RESOURCE_LABELS: Record = { comment: 'Comments', seer: 'Seer', preprod_artifact: 'Preprod Artifacts', + alert: 'Alerts', }; export function webhookResourceLabel(resource: WebhookEvent): string { @@ -98,4 +102,5 @@ export const PERMISSIONS_MAP = { comment: 'Event', seer: 'Event', preprod_artifact: 'Project', + alert: 'Alerts', } as const; diff --git a/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.spec.tsx index d8c964f8527f..403f35214159 100644 --- a/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.spec.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.spec.tsx @@ -1,3 +1,5 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; + import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {Form} from 'sentry/components/forms/form'; @@ -112,6 +114,36 @@ describe('Resource Subscriptions', () => { }); }); + describe('alert subscriptions', () => { + const organization = OrganizationFixture({features: ['sentry-app-alert-webhooks']}); + + it('requires Alerts permission', () => { + render( +
+ + , + {organization} + ); + + expect(screen.getByRole('checkbox', {name: 'alert'})).toBeDisabled(); + }); + + it('enables with Alerts read permission', () => { + render( +
+ + , + {organization} + ); + + expect(screen.getByRole('checkbox', {name: 'alert'})).toBeEnabled(); + }); + }); + describe('initial access to permissions', () => { it('renders nondisabled checkbox with correct permissions', () => { render( diff --git a/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.tsx b/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.tsx index eeead03a9a1b..551253391f56 100644 --- a/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/resourceSubscriptions.tsx @@ -14,6 +14,11 @@ import {SubscriptionBox} from './subscriptionBox'; type Resource = (typeof EVENT_CHOICES)[number]; +// Optional permissions, like Alerts, are missing until an app sets them. +function hasNoAccess(permissions: Permissions, resource: Resource): boolean { + return (permissions[PERMISSIONS_MAP[resource]] ?? 'no-access') === 'no-access'; +} + type Props = { events: WebhookSubscription[]; onChange: (events: WebhookSubscription[]) => void; @@ -25,9 +30,7 @@ export function Subscriptions({events, onChange, permissions}: Props) { useEffect(() => { const permitted = new Set( EVENT_CHOICES.flatMap(resource => - permissions[PERMISSIONS_MAP[resource]] === 'no-access' - ? [] - : [resource, ...RESOURCE_EVENTS[resource]] + hasNoAccess(permissions, resource) ? [] : [resource, ...RESOURCE_EVENTS[resource]] ) ); const permittedEvents = events.filter(subscription => permitted.has(subscription)); @@ -58,7 +61,7 @@ export function Subscriptions({events, onChange, permissions}: Props) { }; const boxes = EVENT_CHOICES.map(choice => { - const disabledFromPermissions = permissions[PERMISSIONS_MAP[choice]] === 'no-access'; + const disabledFromPermissions = hasNoAccess(permissions, choice); const selectedEvents = disabledFromPermissions ? [] : RESOURCE_EVENTS[choice].filter(event => events.includes(event)); diff --git a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/webhookSubjects.tsx b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/webhookSubjects.tsx index 6ad3dcb5423e..70000984f05c 100644 --- a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/webhookSubjects.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/webhookSubjects.tsx @@ -6,6 +6,7 @@ import {Text} from '@sentry/scraps/text'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import {makeAlertsPathname} from 'sentry/views/alerts/pathnames'; +import {makeAutomationDetailsPathname} from 'sentry/views/automations/pathnames'; const EMPTY_VALUE = '—'; @@ -32,6 +33,10 @@ const SUBJECT_SPECS: Record = { getUrl: (id, organization) => makeAlertsPathname({organization, path: `/rules/details/${id}/`}), }, + workflow: { + label: t('Alert'), + getUrl: (id, organization) => makeAutomationDetailsPathname(organization.slug, id), + }, preprod_artifact: { label: t('Build'), getUrl: (id, organization) => diff --git a/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.spec.tsx index aa51a9e7f7fb..ff83a9389f68 100644 --- a/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.spec.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.spec.tsx @@ -98,4 +98,22 @@ describe('SubscriptionBox', () => { expect(screen.getByRole('checkbox', {name: 'preprod_artifact'})).toBeEnabled(); }); }); + + describe('alert resource subscription', () => { + it('hidden without sentry-app-alert-webhooks flag', () => { + renderComponent({resource: 'alert'}); + + expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); + }); + + it('renders alert checkboxes with sentry-app-alert-webhooks flag', () => { + renderComponent( + {resource: 'alert'}, + {organization: OrganizationFixture({features: ['sentry-app-alert-webhooks']})} + ); + + expect(screen.getByRole('checkbox', {name: 'alert'})).toBeEnabled(); + expect(screen.getByRole('checkbox', {name: 'alert.triggered'})).toBeEnabled(); + }); + }); }); diff --git a/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.tsx b/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.tsx index 1986e401d310..d7bc494314b6 100644 --- a/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/subscriptionBox.tsx @@ -40,6 +40,10 @@ export function SubscriptionBox({ }: Props) { const {features} = useOrganization(); + if (resource === 'alert' && !features.includes('sentry-app-alert-webhooks')) { + return null; + } + let disabled = disabledFromPermissions; let message = t( "Must have at least 'Read' permissions enabled for %s",