Skip to content
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
2 changes: 2 additions & 0 deletions static/app/types/project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export interface DetailedProject extends ProjectSummary {
preprodDistributionPrCommentsEnabledByCustomer?: boolean;
preprodSizeEnabledByCustomer?: boolean;
preprodSizeEnabledQuery?: string | null;
preprodSizePrCommentsEnabled?: boolean;
preprodSizePrCommentsRules?: unknown[];
preprodSizeStatusChecksEnabled?: boolean;
preprodSizeStatusChecksRules?: unknown[];
preprodSnapshotPrCommentsEnabled?: boolean;
Expand Down
11 changes: 11 additions & 0 deletions static/app/utils/analytics/preprodBuildAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ export type PreprodBuildEventParameters = {
'preprod.releases.snapshots.tab-clicked': {
organization: Organization;
};
'preprod.settings.pr_comment_rule_created': PreprodSettingsEvent;
'preprod.settings.pr_comment_rule_deleted': PreprodSettingsEvent;
'preprod.settings.pr_comment_rule_updated': PreprodSettingsEvent & {
artifact_type: ArtifactType;
measurement: string;
metric: string;
value: number;
};
'preprod.settings.status_check_rule_created': PreprodSettingsEvent;
'preprod.settings.status_check_rule_deleted': PreprodSettingsEvent;
'preprod.settings.status_check_rule_updated': PreprodSettingsEvent & {
Expand Down Expand Up @@ -169,4 +177,7 @@ export const preprodBuildEventMap: Record<PreprodBuildAnalyticsKey, string | nul
'Preprod Settings: Status Check Rule Deleted',
'preprod.settings.status_check_rule_updated':
'Preprod Settings: Status Check Rule Updated',
'preprod.settings.pr_comment_rule_created': 'Preprod Settings: PR Comment Rule Created',
'preprod.settings.pr_comment_rule_deleted': 'Preprod Settings: PR Comment Rule Deleted',
'preprod.settings.pr_comment_rule_updated': 'Preprod Settings: PR Comment Rule Updated',
};
4 changes: 4 additions & 0 deletions static/app/views/settings/project/preprod/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {PreprodQuotaAlert} from 'sentry/views/preprod/components/preprodQuotaAle
import {SettingsPageHeader} from 'sentry/views/settings/components/settingsPageHeader';

import {FeatureFilter} from './featureFilter';
import {PrCommentRules} from './prCommentRules';
import {PrCommentsToggle} from './prCommentsToggle';
import {StatusCheckRules} from './statusCheckRules';

Expand Down Expand Up @@ -85,6 +86,9 @@ export default function PreprodSettings() {
docsUrl="https://docs.sentry.io/product/size-analysis/#configuring-size-analysis-uploads"
/>
<StatusCheckRules />
<Feature features="organizations:preprod-size-analysis-pr-comments">
<PrCommentRules />
</Feature>
</Fragment>
)}
{tab === 'distribution' && (
Expand Down
238 changes: 238 additions & 0 deletions static/app/views/settings/project/preprod/prCommentRules.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import {DetailedProjectFixture} from 'sentry-fixture/project';
import {RepositoryFixture} from 'sentry-fixture/repository';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';

import {PrCommentRules} from 'sentry/views/settings/project/preprod/prCommentRules';

describe('PrCommentRules', () => {
const {organization} = initializeOrg();
const initialRouterConfig = {
location: {
pathname: `/settings/projects/test-project/preprod/`,
},
route: '/settings/projects/:projectId/preprod/',
};

function mockRepositories(repositories = [RepositoryFixture()]) {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/repos/`,
body: repositories,
});
}

beforeEach(() => {
MockApiClient.clearMockResponses();
});

it('renders disabled by default when the project has no preprod options', async () => {
mockRepositories();
const project = DetailedProjectFixture({options: {}});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: project,
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

expect(
await screen.findByRole('checkbox', {name: 'Toggle PR comments'})
).not.toBeChecked();
expect(
screen.getByText('Enable PR comments above to configure rules.')
).toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'Create PR Comment Rule'})
).not.toBeInTheDocument();
});

it('reflects an enabled project from the explicit field', async () => {
mockRepositories();
const project = DetailedProjectFixture({
options: {},
preprodSizePrCommentsEnabled: true,
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: project,
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

expect(
await screen.findByRole('checkbox', {name: 'Toggle PR comments'})
).toBeChecked();
expect(
screen.getByText('No PR comment rules configured. Create one to get started.')
).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Create PR Comment Rule'})
).toBeInTheDocument();
});

it('reflects an enabled project from the option key when the explicit field is absent', async () => {
mockRepositories();
const project = DetailedProjectFixture({
options: {'sentry:preprod_size_pr_comments_enabled': true},
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: project,
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

expect(
await screen.findByRole('checkbox', {name: 'Toggle PR comments'})
).toBeChecked();
expect(
screen.getByRole('button', {name: 'Create PR Comment Rule'})
).toBeInTheDocument();
});

it('reflects a disabled project from the option key when the explicit field is absent', async () => {
mockRepositories();
const project = DetailedProjectFixture({
options: {'sentry:preprod_size_pr_comments_enabled': false},
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: project,
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

expect(
await screen.findByRole('checkbox', {name: 'Toggle PR comments'})
).not.toBeChecked();
expect(
screen.getByText('Enable PR comments above to configure rules.')
).toBeInTheDocument();
});

it('enables PR comments when toggled on', async () => {
mockRepositories();
const project = DetailedProjectFixture({options: {}});
const projectEndpoint = `/projects/${organization.slug}/${project.slug}/`;
MockApiClient.addMockResponse({
url: projectEndpoint,
body: project,
});
const mock = MockApiClient.addMockResponse({
url: projectEndpoint,
method: 'PUT',
body: {},
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

await userEvent.click(
await screen.findByRole('checkbox', {name: 'Toggle PR comments'})
);

await waitFor(() =>
expect(mock).toHaveBeenCalledWith(
projectEndpoint,
expect.objectContaining({
method: 'PUT',
data: {preprodSizePrCommentsEnabled: true},
})
)
);
});

it('creates a rule when the create button is clicked', async () => {
mockRepositories();
const project = DetailedProjectFixture({
options: {},
preprodSizePrCommentsEnabled: true,
});
const projectEndpoint = `/projects/${organization.slug}/${project.slug}/`;
MockApiClient.addMockResponse({
url: projectEndpoint,
body: project,
});
const mock = MockApiClient.addMockResponse({
url: projectEndpoint,
method: 'PUT',
body: {},
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

await userEvent.click(
await screen.findByRole('button', {name: 'Create PR Comment Rule'})
);

await waitFor(() =>
expect(mock).toHaveBeenCalledWith(
projectEndpoint,
expect.objectContaining({
method: 'PUT',
data: {
preprodSizePrCommentsRules: [
expect.objectContaining({
metric: 'install_size',
measurement: 'absolute',
value: 0,
artifactType: 'main_artifact',
}),
],
},
})
)
);
});

it('shows the connect-a-repository empty state when there are no repositories', async () => {
mockRepositories([]);
const project = DetailedProjectFixture({
options: {},
preprodSizePrCommentsEnabled: true,
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: project,
});

render(<PrCommentRules />, {
organization,
outletContext: {project},
initialRouterConfig,
});

expect(
await screen.findByText(
'Connect at least one repository to get Size Analysis PR comments'
)
).toBeInTheDocument();
expect(
screen.queryByRole('checkbox', {name: 'Toggle PR comments'})
).not.toBeInTheDocument();
});
});
81 changes: 81 additions & 0 deletions static/app/views/settings/project/preprod/prCommentRules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjectSettingsOutlet} from 'sentry/views/settings/project/projectSettingsLayout';

import {SizeRulesPanel} from './sizeRulesPanel';
import {DEFAULT_ARTIFACT_TYPE} from './types';

export function PrCommentRules() {
const organization = useOrganization();
const {project} = useProjectSettingsOutlet();

return (
<SizeRulesPanel
config={{
rules: {
enabledField: 'preprodSizePrCommentsEnabled',
enabledOptionKey: 'sentry:preprod_size_pr_comments_enabled',
defaultEnabled: false,
rulesField: 'preprodSizePrCommentsRules',
rulesOptionKey: 'sentry:preprod_size_pr_comments_rules',
toasts: {
enabled: t('PR comments enabled.'),
disabled: t('PR comments disabled.'),
created: t('PR comment rule created.'),
saved: t('PR comment rule saved.'),
deleted: t('PR comment rule deleted.'),
},
},
copy: {
panelTitle: t('Size Analysis - PR Comments'),
enabledLabel: t('PR Comments Enabled'),
enabledDescription: t(
"Sentry will post PR comments based on your build's app size."
),
toggleAriaLabel: t('Toggle PR comments'),
emptyRulesText: t('No PR comment rules configured. Create one to get started.'),
disabledHintText: t('Enable PR comments above to configure rules.'),
addRuleButtonLabel: t('Create PR Comment Rule'),
connectRepoText: t(
'Connect at least one repository to get Size Analysis PR comments'
),
form: {
headerLabel: t('Comment on PR When'),
deleteConfirmHeader: t(
'Are you sure you want to delete this PR comment rule?'
),
deleteConfirmMessage: (ruleDescription, valueWithUnit) => (
<span>
Will no longer comment on PRs when <strong>{ruleDescription}</strong>{' '}
surpasses <strong>{valueWithUnit}</strong>
</span>
),
searchSource: 'preprod_pr_comment_filters',
},
},
analytics: {
onCreate: () =>
trackAnalytics('preprod.settings.pr_comment_rule_created', {
organization,
project_slug: project.slug,
}),
onUpdate: rule =>
trackAnalytics('preprod.settings.pr_comment_rule_updated', {
organization,
project_slug: project.slug,
metric: rule.metric,
measurement: rule.measurement,
artifact_type: rule.artifactType ?? DEFAULT_ARTIFACT_TYPE,
value: rule.value,
}),
onDelete: () =>
trackAnalytics('preprod.settings.pr_comment_rule_deleted', {
organization,
project_slug: project.slug,
}),
},
}}
/>
);
}
Loading
Loading