Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@ import {ProjectsStore} from 'sentry/stores/projectsStore';

describe('ClaudeCodeIntegrationCta', () => {
const project = ProjectFixture();
const enabledProject = ProjectFixture({
...project,
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
const organization = OrganizationFixture({
features: ['integrations-claude-code'],
});

function mockDetailedProject(projectBody = enabledProject) {
return MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectBody.slug}/`,
body: projectBody,
});
}

beforeEach(() => {
MockApiClient.clearMockResponses();
localStorage.clear();

mockDetailedProject();

MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`,
body: {
Expand Down Expand Up @@ -209,6 +223,7 @@ describe('ClaudeCodeIntegrationCta', () => {
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
mockDetailedProject(projectWithAutomation);

const projectUpdateMock = MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithAutomation.slug}/`,
Expand Down Expand Up @@ -256,6 +271,7 @@ describe('ClaudeCodeIntegrationCta', () => {
seerScannerAutomation: false,
autofixAutomationTuning: 'off',
});
mockDetailedProject(projectWithoutAutomation);

const updatedProject = {
...projectWithoutAutomation,
Expand Down Expand Up @@ -370,6 +386,7 @@ describe('ClaudeCodeIntegrationCta', () => {
seerScannerAutomation: false,
autofixAutomationTuning: 'off',
});
mockDetailedProject(projectWithoutAutomation);

render(<ClaudeCodeIntegrationCta project={projectWithoutAutomation} />, {
organization,
Expand Down Expand Up @@ -423,6 +440,7 @@ describe('ClaudeCodeIntegrationCta', () => {
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
mockDetailedProject(projectWithAutomation);

render(<ClaudeCodeIntegrationCta project={projectWithAutomation} />, {
organization,
Expand All @@ -440,6 +458,7 @@ describe('ClaudeCodeIntegrationCta', () => {
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
mockDetailedProject(projectWithAutomation);

render(<ClaudeCodeIntegrationCta project={projectWithAutomation} />, {
organization,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {t, tct} from 'sentry/locale';
import {PluginIcon} from 'sentry/plugins/components/pluginIcon';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useDetailedProject} from 'sentry/utils/project/useDetailedProject';
import {useUpdateProject} from 'sentry/utils/project/useUpdateProject';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useUser} from 'sentry/utils/useUser';
Expand All @@ -40,6 +41,16 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) {
const organization = useOrganization();
const user = useUser();

const hasFeatureFlag =
!config.featureFlag || organization.features.includes(config.featureFlag);
const {data: projectDetails = project, isPending: isLoadingProject} =
useDetailedProject(
{
orgSlug: organization.slug,
projectSlug: project.slug,
},
{enabled: hasFeatureFlag}
);
const {data, isFetching: isLoadingPreferences} = useProjectSeerPreferences(project);
const preference = data?.preference;
const {mutate: updateProjectSeerPreferences, isPending: isUpdatingPreferences} =
Expand All @@ -53,12 +64,10 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) {
i => i.provider === config.provider
);

const hasFeatureFlag =
!config.featureFlag || organization.features.includes(config.featureFlag);
const hasIntegration = Boolean(integration);
const isAutomationEnabled =
project.seerScannerAutomation !== false &&
project.autofixAutomationTuning !== 'off';
projectDetails.seerScannerAutomation !== false &&
projectDetails.autofixAutomationTuning !== 'off';
const isConfigured =
preference?.automation_handoff?.target === config.target && isAutomationEnabled;

Expand Down Expand Up @@ -86,8 +95,8 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) {
});

const isAutomationDisabled =
project.seerScannerAutomation === false ||
project.autofixAutomationTuning === 'off';
projectDetails.seerScannerAutomation === false ||
projectDetails.autofixAutomationTuning === 'off';

if (isAutomationDisabled) {
await updateProjectAutomation({
Expand All @@ -111,7 +120,12 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) {
return null;
}

if (isLoadingPreferences || isLoadingIntegrations || isUpdatingPreferences) {
if (
isLoadingProject ||
isLoadingPreferences ||
isLoadingIntegrations ||
isUpdatingPreferences
) {
return (
<Container
padding="xl"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ import {ProjectsStore} from 'sentry/stores/projectsStore';

describe('CursorIntegrationCta', () => {
const project = ProjectFixture();
const enabledProject = ProjectFixture({
...project,
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
const organization = OrganizationFixture();

beforeEach(() => {
MockApiClient.clearMockResponses();
localStorage.clear();

MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${enabledProject.slug}/`,
body: enabledProject,
});

// Default mock for seer preferences
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`,
Expand Down Expand Up @@ -187,6 +197,10 @@ describe('CursorIntegrationCta', () => {
seerScannerAutomation: false,
autofixAutomationTuning: 'off',
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithoutAutomation.slug}/`,
body: projectWithoutAutomation,
});

const updatedProject = {
...projectWithoutAutomation,
Expand Down Expand Up @@ -271,6 +285,10 @@ describe('CursorIntegrationCta', () => {
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithAutomation.slug}/`,
body: projectWithAutomation,
});

const projectUpdateMock = MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithAutomation.slug}/`,
Expand Down Expand Up @@ -353,6 +371,10 @@ describe('CursorIntegrationCta', () => {
seerScannerAutomation: false,
autofixAutomationTuning: 'off',
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithoutAutomation.slug}/`,
body: projectWithoutAutomation,
});

render(<CursorIntegrationCta project={projectWithoutAutomation} />, {
organization,
Expand Down Expand Up @@ -409,6 +431,31 @@ describe('CursorIntegrationCta', () => {
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithAutomation.slug}/`,
body: projectWithAutomation,
});

render(<CursorIntegrationCta project={projectWithAutomation} />, {
organization,
});

expect(await screen.findByText('Cursor Agent Integration')).toBeInTheDocument();
expect(screen.getByText(/Cursor handoff is active/)).toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'Set Seer to hand off to Cursor'})
).not.toBeInTheDocument();
});

it('treats missing scanner automation as enabled when tuning is enabled', async () => {
const projectWithAutomation = ProjectFixture({
autofixAutomationTuning: 'medium',
});
delete projectWithAutomation.seerScannerAutomation;
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithAutomation.slug}/`,
body: projectWithAutomation,
});

render(<CursorIntegrationCta project={projectWithAutomation} />, {
organization,
Expand All @@ -426,6 +473,10 @@ describe('CursorIntegrationCta', () => {
seerScannerAutomation: true,
autofixAutomationTuning: 'medium',
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${projectWithAutomation.slug}/`,
body: projectWithAutomation,
});

render(<CursorIntegrationCta project={projectWithAutomation} />, {
organization,
Expand Down
23 changes: 13 additions & 10 deletions static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
);
const {starredViews: views} = useStarredIssueViews();

const detailedProject = useDetailedProject({
const {data: projectDetails, isPending: isLoadingProject} = useDetailedProject({
orgSlug: organization.slug,
projectSlug: project.slug,
});
Expand All @@ -125,11 +125,10 @@
const needsRepoSelection =
repos.length === 0 && !preference?.repositories?.length && !codeMappingRepos?.length;
const needsAutomation =
detailedProject?.data &&
(detailedProject?.data?.autofixAutomationTuning === 'off' ||
detailedProject?.data?.autofixAutomationTuning === undefined ||
detailedProject?.data?.seerScannerAutomation === false ||
detailedProject?.data?.seerScannerAutomation === undefined);
projectDetails !== undefined &&
(projectDetails.autofixAutomationTuning === 'off' ||
projectDetails.autofixAutomationTuning === undefined ||
projectDetails.seerScannerAutomation === false);
const needsFixabilityView =
!views.some(view => view.query.includes(FieldKey.ISSUE_SEER_ACTIONABILITY)) &&
isStarredViewAllowed;
Expand Down Expand Up @@ -166,8 +165,8 @@
}

const isAutomationDisabled =
project.seerScannerAutomation === false ||
project.autofixAutomationTuning === 'off';
projectDetails?.seerScannerAutomation === false ||
projectDetails.autofixAutomationTuning === 'off';

Check failure on line 169 in static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx

View workflow job for this annotation

GitHub Actions / @typescript/native-preview

'projectDetails' is possibly 'undefined'.

Check failure on line 169 in static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx

View workflow job for this annotation

GitHub Actions / typescript

'projectDetails' is possibly 'undefined'.
Comment thread
scttcper marked this conversation as resolved.
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated

if (isAutomationDisabled) {
await updateProjectAutomation({
Expand Down Expand Up @@ -197,11 +196,15 @@
const firstIncompleteIdx = incompleteStepIndices[0];
const lastIncompleteIdx = incompleteStepIndices[incompleteStepIndices.length - 1];
const anyStepIncomplete = incompleteStepIndices.length > 0;
const showOnboardingSteps =
!isLoadingPreferences && !isLoadingProject && anyStepIncomplete;
const showCollapsedSummary = showOnboardingSteps && stepsCollapsed;
const showFullGuidedSteps = showOnboardingSteps && !stepsCollapsed;

return (
<Stack align="stretch">
{/* Collapsed summary */}
{!isLoadingPreferences && anyStepIncomplete && stepsCollapsed && (
{showCollapsedSummary && (
<CollapsedSummaryCard onClick={() => setStepsCollapsed(false)}>
<IconSeer animation="waiting" size="lg" style={{marginRight: 8}} />
<span>
Expand All @@ -215,7 +218,7 @@
</CollapsedSummaryCard>
)}
{/* Full guided steps */}
{!isLoadingPreferences && anyStepIncomplete && !stepsCollapsed && (
{showFullGuidedSteps && (
<AnimatePresence>
<motion.div
initial={{opacity: 0, y: 10, height: 0}}
Expand Down
28 changes: 28 additions & 0 deletions static/app/views/settings/projectSeer/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ describe('ProjectSeer', () => {
beforeEach(() => {
project = ProjectFixture();
organization = OrganizationFixture();
MockApiClient.addMockResponse({
url: `/projects/org-slug/${project.slug}/`,
body: project,
});

// Mock the seer setup check endpoint
MockApiClient.addMockResponse({
Expand Down Expand Up @@ -716,6 +720,10 @@ describe('ProjectSeer', () => {
autofixAutomationTuning: 'medium',
seerScannerAutomation: true,
};
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: initialProject,
});

MockApiClient.addMockResponse({
url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
Expand Down Expand Up @@ -795,6 +803,10 @@ describe('ProjectSeer', () => {
autofixAutomationTuning: 'medium',
seerScannerAutomation: true,
};
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: initialProject,
});

MockApiClient.addMockResponse({
url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
Expand Down Expand Up @@ -903,6 +915,10 @@ describe('ProjectSeer', () => {
autofixAutomationTuning: 'medium',
seerScannerAutomation: true,
};
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: initialProject,
});

MockApiClient.addMockResponse({
url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
Expand Down Expand Up @@ -993,6 +1009,10 @@ describe('ProjectSeer', () => {
autofixAutomationTuning: 'medium',
seerScannerAutomation: true,
};
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: initialProject,
});

MockApiClient.addMockResponse({
url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
Expand Down Expand Up @@ -1116,6 +1136,10 @@ describe('ProjectSeer', () => {
autofixAutomationTuning: 'medium',
seerScannerAutomation: true,
};
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: initialProject,
});

MockApiClient.addMockResponse({
url: `/organizations/${orgWithBothFeatures.slug}/seer/setup-check/`,
Expand Down Expand Up @@ -1198,6 +1222,10 @@ describe('ProjectSeer', () => {
autofixAutomationTuning: 'medium',
seerScannerAutomation: true,
};
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/`,
body: initialProject,
});

MockApiClient.addMockResponse({
url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
Expand Down
Loading