Skip to content

feat(autofix): New onboarding/notice flow #91855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 19, 2025
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: 1 addition & 1 deletion static/app/types/project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type Project = {
team: Team;
teams: Team[];
verifySSL: boolean;
autofixAutomationTuning?: 'off' | 'low' | 'medium' | 'high';
autofixAutomationTuning?: 'off' | 'low' | 'medium' | 'high' | 'always';
builtinSymbolSources?: string[];
Comment on lines -67 to 68
Copy link
Member Author

Choose a reason for hiding this comment

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

bug fix

defaultEnvironment?: string;
eventProcessing?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ describe('SeerDrawer', () => {
preference: null,
},
});
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/group-search-views/starred/`,
body: [],
});
});

it('renders consent state if not consented', async () => {
Expand Down Expand Up @@ -217,7 +221,7 @@ describe('SeerDrawer', () => {
);

expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();
expect(screen.getByText('Set Up Now')).toBeInTheDocument();
expect(screen.getByText('Set Up Integration')).toBeInTheDocument();

const startButton = screen.getByRole('button', {name: 'Start Seer'});
expect(startButton).toBeInTheDocument();
Expand Down Expand Up @@ -486,7 +490,7 @@ describe('SeerDrawer', () => {
// Since "Install the GitHub Integration" text isn't found, let's check for
// the "Set Up the GitHub Integration" text which is what the component is actually showing
expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();
expect(screen.getByText('Set Up Now')).toBeInTheDocument();
expect(screen.getByText('Set Up Integration')).toBeInTheDocument();
});

it('does not render SeerNotices when all repositories are readable', async () => {
Expand Down
294 changes: 67 additions & 227 deletions static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import {GroupSearchViewFixture} from 'sentry-fixture/groupSearchView';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';

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

import {useAutofixRepos} from 'sentry/components/events/autofix/useAutofix';
import {SeerNotices} from 'sentry/views/issueDetails/streamline/sidebar/seerNotices';

jest.mock('sentry/components/events/autofix/useAutofix');

describe('SeerNotices', function () {
// Helper function to create repository objects
const createRepository = (overrides = {}) => ({
external_id: 'repo-123',
name: 'org/repo',
Expand All @@ -20,251 +18,93 @@ describe('SeerNotices', function () {
...overrides,
});

const project = ProjectFixture();
function getProjectWithAutomation(
automationTuning = 'off' as 'off' | 'low' | 'medium' | 'high' | 'always'
) {
return {
...ProjectFixture(),
autofixAutomationTuning: automationTuning,
organization: {
...ProjectFixture().organization,
features: ['trigger-autofix-on-issue-summary'],
},
};
}

const organization = OrganizationFixture({
features: ['trigger-autofix-on-issue-summary'],
});

beforeEach(() => {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/projects/${project.organization.slug}/${project.slug}/seer/preferences/`,
url: `/projects/${organization.slug}/${ProjectFixture().slug}/seer/preferences/`,
body: {
code_mapping_repos: [],
preference: null,
},
});

// Reset mock before each test
jest.mocked(useAutofixRepos).mockReset();
});

it('renders nothing when all repositories are readable', function () {
const repositories = [createRepository(), createRepository({name: 'org/repo2'})];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
});

const {container} = render(
<SeerNotices groupId="123" hasGithubIntegration project={project} />
);

expect(container).toBeEmptyDOMElement();
});

it('renders GitHub integration setup card when hasGithubIntegration is false', function () {
jest.mocked(useAutofixRepos).mockReturnValue({
repos: [createRepository()],
codebases: {},
});

render(<SeerNotices groupId="123" hasGithubIntegration={false} project={project} />);

expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();

// Test for text fragments with formatting
expect(screen.getByText(/Seer is/, {exact: false})).toBeInTheDocument();
expect(screen.getByText('a lot better')).toBeInTheDocument();
expect(
screen.getByText(/when it has your codebase as context/, {exact: false})
).toBeInTheDocument();

// Test for text with links
expect(screen.getByText(/Set up the/, {exact: false})).toBeInTheDocument();
expect(screen.getByText('GitHub Integration', {selector: 'a'})).toBeInTheDocument();
expect(
screen.getByText(/to allow Seer to go deeper/, {exact: false})
).toBeInTheDocument();

expect(screen.getByText('Set Up Now')).toBeInTheDocument();
expect(screen.getByRole('img', {name: 'Install'})).toBeInTheDocument();
});

it('renders warning for a single unreadable GitHub repository', function () {
const repositories = [createRepository({is_readable: false})];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

expect(screen.getByText(/Seer can't access the/)).toBeInTheDocument();
expect(screen.getByText('org/repo')).toBeInTheDocument();
expect(screen.getByText(/GitHub integration/)).toBeInTheDocument();
});

it('renders warning for a single unreadable non-GitHub repository', function () {
const repositories = [
createRepository({is_readable: false, provider: 'gitlab', name: 'org/gitlab-repo'}),
];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

expect(screen.getByText(/Seer can't access the/)).toBeInTheDocument();
expect(screen.getByText('org/gitlab-repo')).toBeInTheDocument();
expect(
screen.getByText(/It currently only supports GitHub repositories/)
).toBeInTheDocument();
});

it('renders warning for multiple unreadable repositories (all GitHub)', function () {
const repositories = [
createRepository({is_readable: false, name: 'org/repo1'}),
createRepository({is_readable: false, name: 'org/repo2'}),
];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
expect(screen.getByText('org/repo1, org/repo2')).toBeInTheDocument();
expect(screen.getByText(/For best performance, enable the/)).toBeInTheDocument();
expect(screen.getByText(/GitHub integration/)).toBeInTheDocument();
});

it('renders warning for multiple unreadable repositories (all non-GitHub)', function () {
const repositories = [
createRepository({
is_readable: false,
provider: 'gitlab',
name: 'org/gitlab-repo1',
}),
createRepository({
is_readable: false,
provider: 'bitbucket',
name: 'org/bitbucket-repo2',
}),
];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/group-search-views/starred/`,
body: [],
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
expect(screen.getByText('org/gitlab-repo1, org/bitbucket-repo2')).toBeInTheDocument();
expect(
screen.getByText(/Seer currently only supports GitHub repositories/)
).toBeInTheDocument();
});

it('renders warning for multiple unreadable repositories (mixed GitHub and non-GitHub)', function () {
const repositories = [
createRepository({is_readable: false, name: 'org/github-repo'}),
createRepository({is_readable: false, provider: 'gitlab', name: 'org/gitlab-repo'}),
];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${ProjectFixture().slug}/autofix-repos/`,
body: [createRepository()],
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
expect(screen.getByText('org/github-repo, org/gitlab-repo')).toBeInTheDocument();
expect(screen.getByText(/For best performance, enable the/)).toBeInTheDocument();
expect(screen.getByText(/GitHub integration/)).toBeInTheDocument();
expect(
screen.getByText(/Seer currently only supports GitHub repositories/)
).toBeInTheDocument();
});

it('renders warning for unreadable repositories along with GitHub setup card when no GitHub integration', function () {
const repositories = [
createRepository({is_readable: false, name: 'org/repo1'}),
createRepository({is_readable: false, name: 'org/repo2'}),
];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
it('shows automation step if automation is allowed and tuning is off', () => {
const project = getProjectWithAutomation('off');
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
organization,
});

render(<SeerNotices groupId="123" hasGithubIntegration={false} project={project} />);

// GitHub setup card
expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();
expect(screen.getByText('Set Up Now')).toBeInTheDocument();

// Unreadable repos warning
expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
expect(screen.getByText('org/repo1, org/repo2')).toBeInTheDocument();
expect(screen.getByText('Unleash Automation')).toBeInTheDocument();
expect(screen.getByText('Enable Automation')).toBeInTheDocument();
});

it('renders GitHub integration link correctly', function () {
const repositories = [createRepository({is_readable: false, name: 'org/repo1'})];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
it('does not show automation step if automation is not allowed', () => {
const project = {
...ProjectFixture(),
autofixAutomationTuning: 'off' as const,
organization: {
...ProjectFixture().organization,
features: [],
},
};
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
organization: {...organization, features: []},
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

const integrationLink = screen.getByText('GitHub integration');
expect(integrationLink).toHaveAttribute(
'href',
'/settings/org-slug/integrations/github/'
);
expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument();
});

it('combines multiple notices when necessary', function () {
const repositories = [
createRepository({is_readable: false, name: 'org/repo1'}),
createRepository({is_readable: false, name: 'org/repo2'}),
];
jest.mocked(useAutofixRepos).mockReturnValue({
repos: repositories,
codebases: {},
it('shows fixability view step if automation is allowed and view not starred', () => {
const project = getProjectWithAutomation('high');
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
organization,
});

render(<SeerNotices groupId="123" hasGithubIntegration={false} project={project} />);

// Should have both the GitHub setup card and the unreadable repos warning
const setupCard = screen.getByText('Set Up the GitHub Integration').closest('div');
const warningAlert = screen
.getByText(/Seer can't access these repositories:/)
.closest('div');

expect(setupCard).toBeInTheDocument();
expect(warningAlert).toBeInTheDocument();
expect(setupCard).not.toBe(warningAlert);
expect(screen.getByText('Get Some Quick Wins')).toBeInTheDocument();
expect(screen.getByText('Star Recommended View')).toBeInTheDocument();
});

it('renders repository selection card when no repos are selected but GitHub integration is enabled', async function () {
jest.mocked(useAutofixRepos).mockReturnValue({
repos: [],
codebases: {},
});

it('does not render guided steps if all onboarding steps are complete', () => {
MockApiClient.addMockResponse({
url: `/projects/${project.organization.slug}/${project.slug}/seer/preferences/`,
body: {
code_mapping_repos: null,
preference: null,
},
url: `/organizations/${organization.slug}/group-search-views/starred/`,
body: [
GroupSearchViewFixture({
query: 'is:unresolved issue.seer_actionability:high',
starred: true,
}),
],
});

render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);

await waitFor(() => {
expect(screen.getByText('Pick Repositories to Work In')).toBeInTheDocument();
const project = getProjectWithAutomation('medium');
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
...{organization: {...organization, features: []}},
});

const titleElement = screen.getByText('Pick Repositories to Work In');
const cardDescriptionElement = titleElement.nextElementSibling;
const firstSpanInDescription =
cardDescriptionElement?.querySelector('span:first-child');
expect(firstSpanInDescription?.textContent?.replace(/\s+/g, ' ').trim()).toBe(
'Seer is a lot better when it has your codebase as context.'
);

expect(
screen.getByText(/Open the Project Settings menu in the top right/)
).toBeInTheDocument();
// Should not find any step titles
expect(screen.queryByText('Set Up the GitHub Integration')).not.toBeInTheDocument();
expect(screen.queryByText('Pick Repositories to Work In')).not.toBeInTheDocument();
expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument();
expect(screen.queryByText('Get Some Quick Wins')).not.toBeInTheDocument();
});
});
Loading
Loading