Skip to content

Commit c4a1027

Browse files
authored
feat(autofix): New onboarding/notice flow (#91855)
Instead of a bunch of banners at the top, creates a nice set of guided steps. Also adds steps for enabling automation in settings and for creating the recommended fixability-score-based view. (title says "Debug Faster with Seer," some screenshots are outdated) <img width="796" alt="Screenshot 2025-05-17 at 9 36 47 AM" src="https://github.com/user-attachments/assets/07baf09b-51cd-44cf-91e9-ee1d73161385" /> <img width="790" alt="Screenshot 2025-05-17 at 9 36 49 AM" src="https://github.com/user-attachments/assets/c2d7fa6f-acdc-4b3d-b5c4-1088474b8c83" /> <img width="768" alt="Screenshot 2025-05-17 at 9 36 53 AM" src="https://github.com/user-attachments/assets/59df2406-6fa0-4175-af02-a1d31dc9c8f2" /> <img width="784" alt="Screenshot 2025-05-17 at 11 24 17 AM" src="https://github.com/user-attachments/assets/0939a0ed-bddd-4172-a515-b9202d311c53" /> Also in repo settings, add a link to manage organization repos if there are no repos to add. <img width="292" alt="Screenshot 2025-05-17 at 9 52 38 AM" src="https://github.com/user-attachments/assets/751cfa52-8f96-4076-8ba9-e8bb2f73e98c" />
1 parent 20a952d commit c4a1027

File tree

5 files changed

+425
-426
lines changed

5 files changed

+425
-426
lines changed

static/app/types/project.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export type Project = {
6464
team: Team;
6565
teams: Team[];
6666
verifySSL: boolean;
67-
autofixAutomationTuning?: 'off' | 'low' | 'medium' | 'high';
67+
autofixAutomationTuning?: 'off' | 'low' | 'medium' | 'high' | 'always';
6868
builtinSymbolSources?: string[];
6969
defaultEnvironment?: string;
7070
eventProcessing?: {

static/app/views/issueDetails/streamline/sidebar/seerDrawer.spec.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ describe('SeerDrawer', () => {
132132
preference: null,
133133
},
134134
});
135+
MockApiClient.addMockResponse({
136+
url: `/organizations/${mockProject.organization.slug}/group-search-views/starred/`,
137+
body: [],
138+
});
135139
});
136140

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

219223
expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();
220-
expect(screen.getByText('Set Up Now')).toBeInTheDocument();
224+
expect(screen.getByText('Set Up Integration')).toBeInTheDocument();
221225

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

492496
it('does not render SeerNotices when all repositories are readable', async () => {
Lines changed: 67 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1+
import {GroupSearchViewFixture} from 'sentry-fixture/groupSearchView';
2+
import {OrganizationFixture} from 'sentry-fixture/organization';
13
import {ProjectFixture} from 'sentry-fixture/project';
24

3-
import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
5+
import {render, screen} from 'sentry-test/reactTestingLibrary';
46

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

8-
jest.mock('sentry/components/events/autofix/useAutofix');
9-
109
describe('SeerNotices', function () {
11-
// Helper function to create repository objects
1210
const createRepository = (overrides = {}) => ({
1311
external_id: 'repo-123',
1412
name: 'org/repo',
@@ -20,251 +18,93 @@ describe('SeerNotices', function () {
2018
...overrides,
2119
});
2220

23-
const project = ProjectFixture();
21+
function getProjectWithAutomation(
22+
automationTuning = 'off' as 'off' | 'low' | 'medium' | 'high' | 'always'
23+
) {
24+
return {
25+
...ProjectFixture(),
26+
autofixAutomationTuning: automationTuning,
27+
organization: {
28+
...ProjectFixture().organization,
29+
features: ['trigger-autofix-on-issue-summary'],
30+
},
31+
};
32+
}
33+
34+
const organization = OrganizationFixture({
35+
features: ['trigger-autofix-on-issue-summary'],
36+
});
2437

2538
beforeEach(() => {
39+
MockApiClient.clearMockResponses();
2640
MockApiClient.addMockResponse({
27-
url: `/projects/${project.organization.slug}/${project.slug}/seer/preferences/`,
41+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/seer/preferences/`,
2842
body: {
2943
code_mapping_repos: [],
3044
preference: null,
3145
},
3246
});
33-
34-
// Reset mock before each test
35-
jest.mocked(useAutofixRepos).mockReset();
36-
});
37-
38-
it('renders nothing when all repositories are readable', function () {
39-
const repositories = [createRepository(), createRepository({name: 'org/repo2'})];
40-
jest.mocked(useAutofixRepos).mockReturnValue({
41-
repos: repositories,
42-
codebases: {},
43-
});
44-
45-
const {container} = render(
46-
<SeerNotices groupId="123" hasGithubIntegration project={project} />
47-
);
48-
49-
expect(container).toBeEmptyDOMElement();
50-
});
51-
52-
it('renders GitHub integration setup card when hasGithubIntegration is false', function () {
53-
jest.mocked(useAutofixRepos).mockReturnValue({
54-
repos: [createRepository()],
55-
codebases: {},
56-
});
57-
58-
render(<SeerNotices groupId="123" hasGithubIntegration={false} project={project} />);
59-
60-
expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();
61-
62-
// Test for text fragments with formatting
63-
expect(screen.getByText(/Seer is/, {exact: false})).toBeInTheDocument();
64-
expect(screen.getByText('a lot better')).toBeInTheDocument();
65-
expect(
66-
screen.getByText(/when it has your codebase as context/, {exact: false})
67-
).toBeInTheDocument();
68-
69-
// Test for text with links
70-
expect(screen.getByText(/Set up the/, {exact: false})).toBeInTheDocument();
71-
expect(screen.getByText('GitHub Integration', {selector: 'a'})).toBeInTheDocument();
72-
expect(
73-
screen.getByText(/to allow Seer to go deeper/, {exact: false})
74-
).toBeInTheDocument();
75-
76-
expect(screen.getByText('Set Up Now')).toBeInTheDocument();
77-
expect(screen.getByRole('img', {name: 'Install'})).toBeInTheDocument();
78-
});
79-
80-
it('renders warning for a single unreadable GitHub repository', function () {
81-
const repositories = [createRepository({is_readable: false})];
82-
jest.mocked(useAutofixRepos).mockReturnValue({
83-
repos: repositories,
84-
codebases: {},
85-
});
86-
87-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
88-
89-
expect(screen.getByText(/Seer can't access the/)).toBeInTheDocument();
90-
expect(screen.getByText('org/repo')).toBeInTheDocument();
91-
expect(screen.getByText(/GitHub integration/)).toBeInTheDocument();
92-
});
93-
94-
it('renders warning for a single unreadable non-GitHub repository', function () {
95-
const repositories = [
96-
createRepository({is_readable: false, provider: 'gitlab', name: 'org/gitlab-repo'}),
97-
];
98-
jest.mocked(useAutofixRepos).mockReturnValue({
99-
repos: repositories,
100-
codebases: {},
101-
});
102-
103-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
104-
105-
expect(screen.getByText(/Seer can't access the/)).toBeInTheDocument();
106-
expect(screen.getByText('org/gitlab-repo')).toBeInTheDocument();
107-
expect(
108-
screen.getByText(/It currently only supports GitHub repositories/)
109-
).toBeInTheDocument();
110-
});
111-
112-
it('renders warning for multiple unreadable repositories (all GitHub)', function () {
113-
const repositories = [
114-
createRepository({is_readable: false, name: 'org/repo1'}),
115-
createRepository({is_readable: false, name: 'org/repo2'}),
116-
];
117-
jest.mocked(useAutofixRepos).mockReturnValue({
118-
repos: repositories,
119-
codebases: {},
120-
});
121-
122-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
123-
124-
expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
125-
expect(screen.getByText('org/repo1, org/repo2')).toBeInTheDocument();
126-
expect(screen.getByText(/For best performance, enable the/)).toBeInTheDocument();
127-
expect(screen.getByText(/GitHub integration/)).toBeInTheDocument();
128-
});
129-
130-
it('renders warning for multiple unreadable repositories (all non-GitHub)', function () {
131-
const repositories = [
132-
createRepository({
133-
is_readable: false,
134-
provider: 'gitlab',
135-
name: 'org/gitlab-repo1',
136-
}),
137-
createRepository({
138-
is_readable: false,
139-
provider: 'bitbucket',
140-
name: 'org/bitbucket-repo2',
141-
}),
142-
];
143-
jest.mocked(useAutofixRepos).mockReturnValue({
144-
repos: repositories,
145-
codebases: {},
47+
MockApiClient.addMockResponse({
48+
url: `/organizations/${organization.slug}/group-search-views/starred/`,
49+
body: [],
14650
});
147-
148-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
149-
150-
expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
151-
expect(screen.getByText('org/gitlab-repo1, org/bitbucket-repo2')).toBeInTheDocument();
152-
expect(
153-
screen.getByText(/Seer currently only supports GitHub repositories/)
154-
).toBeInTheDocument();
155-
});
156-
157-
it('renders warning for multiple unreadable repositories (mixed GitHub and non-GitHub)', function () {
158-
const repositories = [
159-
createRepository({is_readable: false, name: 'org/github-repo'}),
160-
createRepository({is_readable: false, provider: 'gitlab', name: 'org/gitlab-repo'}),
161-
];
162-
jest.mocked(useAutofixRepos).mockReturnValue({
163-
repos: repositories,
164-
codebases: {},
51+
MockApiClient.addMockResponse({
52+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/autofix-repos/`,
53+
body: [createRepository()],
16554
});
166-
167-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
168-
169-
expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
170-
expect(screen.getByText('org/github-repo, org/gitlab-repo')).toBeInTheDocument();
171-
expect(screen.getByText(/For best performance, enable the/)).toBeInTheDocument();
172-
expect(screen.getByText(/GitHub integration/)).toBeInTheDocument();
173-
expect(
174-
screen.getByText(/Seer currently only supports GitHub repositories/)
175-
).toBeInTheDocument();
17655
});
17756

178-
it('renders warning for unreadable repositories along with GitHub setup card when no GitHub integration', function () {
179-
const repositories = [
180-
createRepository({is_readable: false, name: 'org/repo1'}),
181-
createRepository({is_readable: false, name: 'org/repo2'}),
182-
];
183-
jest.mocked(useAutofixRepos).mockReturnValue({
184-
repos: repositories,
185-
codebases: {},
57+
it('shows automation step if automation is allowed and tuning is off', () => {
58+
const project = getProjectWithAutomation('off');
59+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
60+
organization,
18661
});
187-
188-
render(<SeerNotices groupId="123" hasGithubIntegration={false} project={project} />);
189-
190-
// GitHub setup card
191-
expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument();
192-
expect(screen.getByText('Set Up Now')).toBeInTheDocument();
193-
194-
// Unreadable repos warning
195-
expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument();
196-
expect(screen.getByText('org/repo1, org/repo2')).toBeInTheDocument();
62+
expect(screen.getByText('Unleash Automation')).toBeInTheDocument();
63+
expect(screen.getByText('Enable Automation')).toBeInTheDocument();
19764
});
19865

199-
it('renders GitHub integration link correctly', function () {
200-
const repositories = [createRepository({is_readable: false, name: 'org/repo1'})];
201-
jest.mocked(useAutofixRepos).mockReturnValue({
202-
repos: repositories,
203-
codebases: {},
66+
it('does not show automation step if automation is not allowed', () => {
67+
const project = {
68+
...ProjectFixture(),
69+
autofixAutomationTuning: 'off' as const,
70+
organization: {
71+
...ProjectFixture().organization,
72+
features: [],
73+
},
74+
};
75+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
76+
organization: {...organization, features: []},
20477
});
205-
206-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
207-
208-
const integrationLink = screen.getByText('GitHub integration');
209-
expect(integrationLink).toHaveAttribute(
210-
'href',
211-
'/settings/org-slug/integrations/github/'
212-
);
78+
expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument();
21379
});
21480

215-
it('combines multiple notices when necessary', function () {
216-
const repositories = [
217-
createRepository({is_readable: false, name: 'org/repo1'}),
218-
createRepository({is_readable: false, name: 'org/repo2'}),
219-
];
220-
jest.mocked(useAutofixRepos).mockReturnValue({
221-
repos: repositories,
222-
codebases: {},
81+
it('shows fixability view step if automation is allowed and view not starred', () => {
82+
const project = getProjectWithAutomation('high');
83+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
84+
organization,
22385
});
224-
225-
render(<SeerNotices groupId="123" hasGithubIntegration={false} project={project} />);
226-
227-
// Should have both the GitHub setup card and the unreadable repos warning
228-
const setupCard = screen.getByText('Set Up the GitHub Integration').closest('div');
229-
const warningAlert = screen
230-
.getByText(/Seer can't access these repositories:/)
231-
.closest('div');
232-
233-
expect(setupCard).toBeInTheDocument();
234-
expect(warningAlert).toBeInTheDocument();
235-
expect(setupCard).not.toBe(warningAlert);
86+
expect(screen.getByText('Get Some Quick Wins')).toBeInTheDocument();
87+
expect(screen.getByText('Star Recommended View')).toBeInTheDocument();
23688
});
23789

238-
it('renders repository selection card when no repos are selected but GitHub integration is enabled', async function () {
239-
jest.mocked(useAutofixRepos).mockReturnValue({
240-
repos: [],
241-
codebases: {},
242-
});
243-
90+
it('does not render guided steps if all onboarding steps are complete', () => {
24491
MockApiClient.addMockResponse({
245-
url: `/projects/${project.organization.slug}/${project.slug}/seer/preferences/`,
246-
body: {
247-
code_mapping_repos: null,
248-
preference: null,
249-
},
92+
url: `/organizations/${organization.slug}/group-search-views/starred/`,
93+
body: [
94+
GroupSearchViewFixture({
95+
query: 'is:unresolved issue.seer_actionability:high',
96+
starred: true,
97+
}),
98+
],
25099
});
251-
252-
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />);
253-
254-
await waitFor(() => {
255-
expect(screen.getByText('Pick Repositories to Work In')).toBeInTheDocument();
100+
const project = getProjectWithAutomation('medium');
101+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
102+
...{organization: {...organization, features: []}},
256103
});
257-
258-
const titleElement = screen.getByText('Pick Repositories to Work In');
259-
const cardDescriptionElement = titleElement.nextElementSibling;
260-
const firstSpanInDescription =
261-
cardDescriptionElement?.querySelector('span:first-child');
262-
expect(firstSpanInDescription?.textContent?.replace(/\s+/g, ' ').trim()).toBe(
263-
'Seer is a lot better when it has your codebase as context.'
264-
);
265-
266-
expect(
267-
screen.getByText(/Open the Project Settings menu in the top right/)
268-
).toBeInTheDocument();
104+
// Should not find any step titles
105+
expect(screen.queryByText('Set Up the GitHub Integration')).not.toBeInTheDocument();
106+
expect(screen.queryByText('Pick Repositories to Work In')).not.toBeInTheDocument();
107+
expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument();
108+
expect(screen.queryByText('Get Some Quick Wins')).not.toBeInTheDocument();
269109
});
270110
});

0 commit comments

Comments
 (0)