Skip to content
Draft
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
8 changes: 7 additions & 1 deletion static/app/types/integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<WebhookEvent, readonly string[]>;

export type WebhookSubscription = (typeof RESOURCE_EVENTS)[WebhookEvent][number];
Expand All @@ -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<string, WebhookSubscription> = {
Expand Down Expand Up @@ -86,6 +89,7 @@ const RESOURCE_LABELS: Record<WebhookEvent, string> = {
comment: 'Comments',
seer: 'Seer',
preprod_artifact: 'Preprod Artifacts',
alert: 'Alerts',
};

export function webhookResourceLabel(resource: WebhookEvent): string {
Expand All @@ -98,4 +102,5 @@ export const PERMISSIONS_MAP = {
comment: 'Event',
seer: 'Event',
preprod_artifact: 'Project',
alert: 'Alerts',
} as const;
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -112,6 +114,36 @@ describe('Resource Subscriptions', () => {
});
});

describe('alert subscriptions', () => {
const organization = OrganizationFixture({features: ['sentry-app-alert-webhooks']});

it('requires Alerts permission', () => {
render(
<Form>
<Subscriptions events={[]} permissions={basePermissions} onChange={jest.fn()} />
</Form>,
{organization}
);

expect(screen.getByRole('checkbox', {name: 'alert'})).toBeDisabled();
});

it('enables with Alerts read permission', () => {
render(
<Form>
<Subscriptions
events={[]}
permissions={{...basePermissions, Alerts: 'read'}}
onChange={jest.fn()}
/>
</Form>,
{organization}
);

expect(screen.getByRole('checkbox', {name: 'alert'})).toBeEnabled();
});
});

describe('initial access to permissions', () => {
it('renders nondisabled checkbox with correct permissions', () => {
render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,9 +30,7 @@ export function Subscriptions({events, onChange, permissions}: Props) {
useEffect(() => {
const permitted = new Set<string>(
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));
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '—';

Expand All @@ -32,6 +33,10 @@ const SUBJECT_SPECS: Record<string, SubjectSpec> = {
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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading