Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import {selectEvent} from 'sentry-test/selectEvent';

import {Form} from 'sentry/components/forms/form';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {ActionGroup, ActionType} from 'sentry/types/workflowEngine/actions';
import {
DataConditionHandlerGroupType,
Expand All @@ -37,6 +38,7 @@ describe('AutomateSection', () => {
beforeEach(() => {
jest.resetAllMocks();
MockApiClient.clearMockResponses();
ProjectsStore.loadInitialData([project]);

MockApiClient.addMockResponse({
url: '/organizations/org-slug/workflows/',
Expand Down Expand Up @@ -130,8 +132,8 @@ describe('AutomateSection', () => {

it('can connect an existing automation', async () => {
render(
<DetectorFormProvider detectorType="metric_issue" project={project}>
<Form>
<DetectorFormProvider detectorType="metric_issue">
<Form initialData={{projectId: project.id}}>
<AutomateSection />
</Form>
</DetectorFormProvider>
Expand Down Expand Up @@ -165,8 +167,8 @@ describe('AutomateSection', () => {

it('can disconnect an existing automation', async () => {
render(
<DetectorFormProvider detectorType="metric_issue" project={project}>
<Form initialData={{workflowIds: [automation1.id]}}>
<DetectorFormProvider detectorType="metric_issue">
<Form initialData={{projectId: project.id, workflowIds: [automation1.id]}}>
<AutomateSection />
</Form>
</DetectorFormProvider>
Expand Down Expand Up @@ -217,8 +219,8 @@ describe('AutomateSection', () => {
});

render(
<DetectorFormProvider detectorType="metric_issue" project={project}>
<Form>
<DetectorFormProvider detectorType="metric_issue">
<Form initialData={{projectId: project.id}}>
<AutomateSection />
</Form>
</DetectorFormProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {t, tct} from 'sentry/locale';
import {AutomationBuilderDrawerForm} from 'sentry/views/automations/components/automationBuilderDrawerForm';
import {ConnectAutomationsDrawer} from 'sentry/views/detectors/components/connectAutomationsDrawer';
import {ConnectedAutomationsList} from 'sentry/views/detectors/components/connectedAutomationList';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject';

export function AutomateSection({step}: {step?: number}) {
const ref = useRef<HTMLButtonElement>(null);
const formContext = useContext(FormContext);
const {openDrawer, closeDrawer, isDrawerOpen} = useDrawer();

const {project} = useDetectorFormContext();
const project = useDetectorFormProject();
const workflowIds = useFormField('workflowIds') as string[];
const setWorkflowIds = useCallback(
(newWorkflowIds: string[]) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import styled from '@emotion/styled';
import {SentryProjectSelectorField} from 'sentry/components/forms/fields/sentryProjectSelectorField';
import {t} from 'sentry/locale';
import {useProjects} from 'sentry/utils/useProjects';
import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {useCanEditDetector} from 'sentry/views/detectors/utils/useCanEditDetector';

export function ProjectField() {
const {projects, fetching} = useProjects();
const {project, detectorType} = useDetectorFormContext();
const {detectorType} = useDetectorFormContext();
const project = useDetectorFormProject();
const canEditDetector = useCanEditDetector({projectId: project.id, detectorType});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {useFormField} from 'sentry/components/workflowEngine/form/useFormField';
import type {Project} from 'sentry/types/project';
import {useProjects} from 'sentry/utils/useProjects';

/**
* Returns the current project based on the form's `projectId` field.
* Reactively updates when the user changes the project in the form.
*
* Must be used within a Form that has a `projectId` field.
*/
export function useDetectorFormProject(): Project {
const projectId = useFormField<string>('projectId');
const {projects} = useProjects();
const project = projects.find(p => p.id === projectId);
if (!project) {
throw new Error(
`useDetectorFormProject: no project found for projectId "${projectId}"`
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The useDetectorFormProject hook throws an unhandled error if no project is found, crashing the component when an organization has zero projects or the projectId is invalid.
Severity: HIGH

Suggested Fix

Instead of throwing an error from the useDetectorFormProject hook, return undefined. Components using this hook should then check for an undefined project and render an appropriate error state, similar to the previous implementation which showed a <LoadingError> component.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
static/app/views/detectors/components/forms/common/useDetectorFormProject.tsx#L14-L19

Potential issue: The new `useDetectorFormProject` hook throws an unhandled error if a
project is not found for a given `projectId`. This occurs in realistic scenarios, such
as when a user in an organization with zero projects attempts to create a new detector.
In this case, `projectId` defaults to an empty string, `projects.find()` returns
`undefined`, and the hook throws an error that crashes the component. The previous
implementation handled this case gracefully by displaying a 'Project not found' message,
but this error handling was removed in the refactor.

Did we get this right? 👍 / 👎 to inform future reviews.

return project;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('useSetAutomaticName', () => {

const renderDetectorForm = (detector?: Detector, initialFormData = {}) => {
return render(
<DetectorFormProvider detectorType="error" project={project} detector={detector}>
<DetectorFormProvider detectorType="error" detector={detector}>
<NewDetectorLayout
detectorType="error"
formDataToEndpointPayload={data => data as any}
Expand Down
6 changes: 1 addition & 5 deletions static/app/views/detectors/components/forms/context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {createContext, useContext, useState} from 'react';

import type {Project} from 'sentry/types/project';
import type {Detector, DetectorType} from 'sentry/types/workflowEngine/detectors';

type DetectorFormContextType = {
Expand All @@ -10,7 +9,6 @@ type DetectorFormContextType = {
* Used by useSetAutomaticName to disable automatic name generation.
*/
hasSetDetectorName: boolean;
project: Project;
setHasSetDetectorName: (value: boolean) => void;
detector?: Detector;
};
Expand All @@ -19,20 +17,18 @@ const DetectorFormContext = createContext<DetectorFormContextType | null>(null);

export function DetectorFormProvider({
detectorType,
project,
detector,
children,
}: {
children: React.ReactNode;
detectorType: DetectorType;
project: Project;
detector?: Detector;
}) {
const [hasSetDetectorName, setHasSetDetectorName] = useState(false);

return (
<DetectorFormContext.Provider
value={{detectorType, project, hasSetDetectorName, setHasSetDetectorName, detector}}
value={{detectorType, hasSetDetectorName, setHasSetDetectorName, detector}}
>
{children}
</DetectorFormContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {t} from 'sentry/locale';
import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview';
import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection';
import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject';
import {useCronDetectorFormField} from 'sentry/views/detectors/components/forms/cron/fields';

const FALLBACK_ISSUE_TITLE = t('Cron failure: …');
Expand All @@ -22,7 +22,7 @@ export function CronIssuePreview({step}: {step?: number}) {
const owner = useCronDetectorFormField('owner');
const issueTitle = useCronIssueTitle();
const assignee = ownerToActor(owner);
const {project} = useDetectorFormContext();
const project = useDetectorFormProject();

return (
<IssuePreviewSection step={step}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('NewCronDetectorForm', () => {

const renderForm = (routerConfig?: any) => {
return render(
<DetectorFormProvider detectorType="monitor_check_in_failure" project={project}>
<DetectorFormProvider detectorType="monitor_check_in_failure">
<NewCronDetectorForm />
</DetectorFormProvider>,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export function EditExistingErrorDetectorForm({detector}: {detector: ErrorDetect
<EditLayout
formProps={{
initialData: {
projectId: detector.projectId,
workflowIds: detector.workflowIds,
},
onSubmit: handleFormSubmit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {selectEvent} from 'sentry-test/selectEvent';

import {MemberListStore} from 'sentry/stores/memberListStore';
import {OrganizationStore} from 'sentry/stores/organizationStore';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {DetectorFormProvider} from 'sentry/views/detectors/components/forms/context';
import {NewMetricDetectorForm} from 'sentry/views/detectors/components/forms/metric/metric';

Expand All @@ -20,6 +21,7 @@ describe('NewMetricDetectorForm', () => {
MockApiClient.clearMockResponses();
OrganizationStore.reset();
OrganizationStore.onUpdate(organization);
ProjectsStore.loadInitialData([project]);
MemberListStore.init();
MemberListStore.loadInitialData([UserFixture()]);

Expand Down Expand Up @@ -80,7 +82,7 @@ describe('NewMetricDetectorForm', () => {

it('shows default issue preview and updates subtitle when threshold changes', async () => {
render(
<DetectorFormProvider detectorType="metric_issue" project={project}>
<DetectorFormProvider detectorType="metric_issue">
<NewMetricDetectorForm />
</DetectorFormProvider>,
{organization}
Expand Down Expand Up @@ -143,7 +145,7 @@ describe('NewMetricDetectorForm', () => {

it('removes is filters when switching away from the errors dataset', async () => {
render(
<DetectorFormProvider detectorType="metric_issue" project={project}>
<DetectorFormProvider detectorType="metric_issue">
<NewMetricDetectorForm />
</DetectorFormProvider>,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {getDuration} from 'sentry/utils/duration/getDuration';
import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview';
import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection';
import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject';
import {
METRIC_DETECTOR_FORM_FIELDS,
useMetricDetectorFormField,
Expand Down Expand Up @@ -81,7 +81,7 @@ export function MetricIssuePreview({step}: {step?: number}) {
const owner = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.owner);
const subtitle = useMetricIssuePreviewSubtitle();
const assignee = ownerToActor(owner);
const {project} = useDetectorFormContext();
const project = useDetectorFormProject();

return (
<IssuePreviewSection step={step}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useMemo, useState} from 'react';
import {useTheme} from '@emotion/react';
import orderBy from 'lodash/orderBy';

import type {FormProps} from 'sentry/components/forms/form';
import {FormModel} from 'sentry/components/forms/model';
Expand All @@ -11,10 +12,10 @@ import type {
DetectorType,
} from 'sentry/types/workflowEngine/detectors';
import {useLocation} from 'sentry/utils/useLocation';
import {useProjects} from 'sentry/utils/useProjects';
import {NewDetectorBreadcrumbs} from 'sentry/views/detectors/components/forms/common/breadcrumbs';
import {DetectorNameField} from 'sentry/views/detectors/components/forms/common/detectorNameField';
import {NewDetectorFooter} from 'sentry/views/detectors/components/forms/common/footer';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {MonitorFeedbackButton} from 'sentry/views/detectors/components/monitorFeedbackButton';
import {useCreateDetectorFormSubmit} from 'sentry/views/detectors/hooks/useCreateDetectorFormSubmit';

Expand Down Expand Up @@ -45,7 +46,19 @@ export function NewDetectorLayout<
const location = useLocation();
const theme = useTheme();
const maxWidth = theme.breakpoints.xl;
const formContext = useDetectorFormContext();
const {projects} = useProjects();

const initialProjectId = useMemo(() => {
const queryProjectId = location.query.project as string | undefined;
if (queryProjectId) {
const match = projects.find(p => p.id === queryProjectId);
if (match) {
return match.id;
}
}
const sorted = orderBy(projects, ['isMember', 'isBookmarked'], ['desc', 'desc']);
return sorted[0]?.id ?? '';
}, [location.query.project, projects]);

const formSubmitHandler = useCreateDetectorFormSubmit({
detectorType,
Expand All @@ -57,15 +70,15 @@ export function NewDetectorLayout<

const initialData = useMemo(() => {
return {
projectId: formContext.project.id,
projectId: initialProjectId,
environment: (location.query.environment as string | undefined) || '',
name: (location.query.name as string | undefined) || '',
owner: (location.query.owner as string | undefined) || '',
workflowIds: [],
...initialFormData,
};
}, [
formContext.project.id,
initialProjectId,
initialFormData,
location.query.environment,
location.query.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {t} from 'sentry/locale';
import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview';
import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection';
import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject';
import {useUptimeDetectorFormField} from 'sentry/views/detectors/components/forms/uptime/fields';
import {formatUptimeUrl} from 'sentry/views/detectors/components/forms/uptime/formatUptimeUrl';

Expand All @@ -28,7 +28,7 @@ export function UptimeIssuePreview({step}: {step?: number}) {
const owner = useUptimeDetectorFormField('owner');
const issueTitle = useUptimeIssueTitle();
const assignee = ownerToActor(owner);
const {project} = useDetectorFormContext();
const project = useDetectorFormProject();

return (
<IssuePreviewSection step={step}>
Expand Down
14 changes: 2 additions & 12 deletions static/app/views/detectors/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {useDetectorQuery} from 'sentry/views/detectors/hooks';

export default function DetectorEdit() {
const params = useParams<{detectorId: string}>();
const {fetching: isFetchingProjects} = useProjects();
useWorkflowEngineFeatureGate({redirect: true});

const {
Expand All @@ -20,9 +21,6 @@ export default function DetectorEdit() {
refetch,
} = useDetectorQuery(params.detectorId);

const {projects, fetching: isFetchingProjects} = useProjects();
const project = projects.find(p => p.id === detector?.projectId);

if (isPending || isFetchingProjects) {
return <LoadingIndicator />;
}
Expand All @@ -36,16 +34,8 @@ export default function DetectorEdit() {
);
}

if (!project) {
return <LoadingError message={t('Project not found')} />;
}

return (
<DetectorFormProvider
detectorType={detector.type}
project={project}
detector={detector}
>
<DetectorFormProvider detectorType={detector.type} detector={detector}>
<EditExistingDetectorForm detector={detector} />
</DetectorFormProvider>
);
Expand Down
Loading
Loading