diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx
index 3b39bcda1596c4..fdcd0205d9601f 100644
--- a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx
+++ b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx
@@ -1,7 +1,7 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
-import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
+import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
import PageFiltersStore from 'sentry/stores/pageFiltersStore';
import ProjectsStore from 'sentry/stores/projectsStore';
@@ -14,8 +14,6 @@ describe('PageOverviewSidebar', () => {
const organization = OrganizationFixture({
features: ['performance-web-vitals-seer-suggestions', 'gen-ai-features'],
});
- let userIssueMock: jest.Mock;
- let eventsMock: jest.Mock;
let seerSetupCheckMock: jest.Mock;
let seerPreferencesMock: jest.Mock;
@@ -53,13 +51,6 @@ describe('PageOverviewSidebar', () => {
},
});
- eventsMock = MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/events/`,
- body: {
- data: [{trace: '123', timestamp: '2025-01-01T00:00:00Z'}],
- },
- });
-
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/issues/`,
body: [
@@ -89,12 +80,6 @@ describe('PageOverviewSidebar', () => {
},
},
});
-
- userIssueMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/user-issue/`,
- body: {event_id: '123'},
- method: 'POST',
- });
});
afterEach(() => {
@@ -218,110 +203,5 @@ describe('PageOverviewSidebar', () => {
).toBeInTheDocument();
expect(screen.getByText('View Suggestion')).toBeInTheDocument();
});
-
- it('should create issues when run seer analysis button is clicked', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/issues/`,
- body: [],
- });
- render(
- ,
- {organization}
- );
- const runSeerAnalysisButton = await screen.findByText('Run Seer Analysis');
- expect(runSeerAnalysisButton).toBeInTheDocument();
- await userEvent.click(runSeerAnalysisButton);
- expect(userIssueMock).toHaveBeenCalledWith(
- '/projects/org-slug/project-slug/user-issue/',
- expect.objectContaining({
- method: 'POST',
- data: expect.objectContaining({
- issueType: 'web_vitals',
- vital: 'lcp',
- score: 80,
- value: 1000,
- transaction: TRANSACTION_NAME,
- }),
- })
- );
- expect(screen.queryByText('Run Seer Analysis')).not.toBeInTheDocument();
- });
-
- it('should create multiple issues with trace ids when run seer analysis button is clicked', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/issues/`,
- body: [],
- });
- render(
- ,
- {organization}
- );
-
- const runSeerAnalysisButton = await screen.findByText('Run Seer Analysis');
- expect(runSeerAnalysisButton).toBeInTheDocument();
- expect(eventsMock).toHaveBeenCalledTimes(5);
- await userEvent.click(runSeerAnalysisButton);
- ['lcp', 'cls', 'fcp', 'inp'].forEach(vital => {
- expect(userIssueMock).toHaveBeenCalledWith(
- '/projects/org-slug/project-slug/user-issue/',
- expect.objectContaining({
- method: 'POST',
- data: expect.objectContaining({
- issueType: 'web_vitals',
- vital,
- score: 80,
- transaction: TRANSACTION_NAME,
- traceId: '123',
- }),
- })
- );
- });
- // TTFB has a score over 90, so it should not be created as an issue
- expect(userIssueMock).not.toHaveBeenCalledWith(
- '/projects/org-slug/project-slug/user-issue/',
- expect.objectContaining({
- method: 'POST',
- data: expect.objectContaining({
- issueType: 'web_vitals',
- vital: 'ttfb',
- score: 100,
- transaction: TRANSACTION_NAME,
- traceId: '123',
- timestamp: '2025-01-01T00:00:00Z',
- }),
- })
- );
- expect(screen.queryByText('Run Seer Analysis')).not.toBeInTheDocument();
- });
});
});
diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx
index cd7e7017f01e27..2c77cd32461cf0 100644
--- a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx
+++ b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx
@@ -1,4 +1,4 @@
-import {Fragment, useMemo, useState} from 'react';
+import {Fragment, useMemo} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
@@ -6,14 +6,10 @@ import ChartZoom from 'sentry/components/charts/chartZoom';
import type {LineChartSeries} from 'sentry/components/charts/lineChart';
import {LineChart} from 'sentry/components/charts/lineChart';
import {FeatureBadge} from 'sentry/components/core/badge/featureBadge';
-import {Button} from 'sentry/components/core/button';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import {Flex} from 'sentry/components/core/layout';
import {ExternalLink} from 'sentry/components/core/link';
-import {
- makeAutofixQueryKey,
- type AutofixResponse,
-} from 'sentry/components/events/autofix/useAutofix';
+import type {AutofixData} from 'sentry/components/events/autofix/types';
import {
getRootCauseCopyText,
getRootCauseDescription,
@@ -28,24 +24,17 @@ import {IconSeer} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {PageFilters} from 'sentry/types/core';
import type {SeriesDataUnit} from 'sentry/types/echarts';
-import {IssueType, type Group} from 'sentry/types/group';
+import type {Group} from 'sentry/types/group';
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
-import {useApiQuery} from 'sentry/utils/queryClient';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import PerformanceScoreRingWithTooltips from 'sentry/views/insights/browser/webVitals/components/performanceScoreRingWithTooltips';
-import type {ProjectData} from 'sentry/views/insights/browser/webVitals/components/webVitalMeters';
import {useProjectRawWebVitalsValuesTimeseriesQuery} from 'sentry/views/insights/browser/webVitals/queries/rawWebVitalsQueries/useProjectRawWebVitalsValuesTimeseriesQuery';
-import {useSampleWebVitalTraceParallel} from 'sentry/views/insights/browser/webVitals/queries/useSampleWebVitalTrace';
-import {
- POLL_INTERVAL,
- useWebVitalsIssuesQuery,
-} from 'sentry/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery';
import {MODULE_DOC_LINK} from 'sentry/views/insights/browser/webVitals/settings';
import type {ProjectScore} from 'sentry/views/insights/browser/webVitals/types';
import type {BrowserType} from 'sentry/views/insights/browser/webVitals/utils/queryParameterDecoders/browserType';
import {useHasSeerWebVitalsSuggestions} from 'sentry/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions';
-import {useRunSeerAnalysis} from 'sentry/views/insights/browser/webVitals/utils/useRunSeerAnalysis';
+import {useSeerWebVitalsSuggestions} from 'sentry/views/insights/browser/webVitals/utils/useSeerWebVitalsSuggestions';
import type {SubregionCode} from 'sentry/views/insights/types';
import {SidebarSpacer} from 'sentry/views/performance/transactionSummary/utils';
@@ -54,7 +43,6 @@ const CHART_HEIGHTS = 100;
type Props = {
transaction: string;
browserTypes?: BrowserType[];
- projectData?: ProjectData[];
projectScore?: ProjectScore;
projectScoreIsLoading?: boolean;
search?: string;
@@ -67,17 +55,11 @@ export function PageOverviewSidebar({
projectScoreIsLoading,
browserTypes,
subregions,
- projectData,
}: Props) {
const hasSeerWebVitalsSuggestions = useHasSeerWebVitalsSuggestions();
const theme = useTheme();
const pageFilters = usePageFilters();
const {period, start, end, utc} = pageFilters.selection.datetime;
- const [isCreatingIssues, setIsCreatingIssues] = useState(false);
- // Event IDs of issues created by the user on this page. Used to control polling logic.
- const [newlyCreatedIssueEventIds, setNewlyCreatedIssueEventIds] = useState<
- string[] | undefined
- >(undefined);
const {data, isLoading: isLoading} = useProjectRawWebVitalsValuesTimeseriesQuery({
transaction,
@@ -154,38 +136,14 @@ export function PageOverviewSidebar({
const ringSegmentColors = theme.chart.getColorPalette(4);
const ringBackgroundColors = ringSegmentColors.map(color => `${color}50`);
- const {data: issues} = useWebVitalsIssuesQuery({
- issueTypes: [IssueType.WEB_VITALS],
+ const {
+ data: {issues, autofix},
+ isLoading: isLoadingAutofix,
+ } = useSeerWebVitalsSuggestions({
transaction,
enabled: hasSeerWebVitalsSuggestions,
- // We only poll for issues if we've created them in this session, otherwise we only attempt to load any existing issues once
- pollInterval: POLL_INTERVAL,
- eventIds: newlyCreatedIssueEventIds,
- });
-
- const {isLoading: isLoadingWebVitalTraceSamples, ...webVitalTraceSamples} =
- useSampleWebVitalTraceParallel({
- transaction,
- projectData,
- enabled: hasSeerWebVitalsSuggestions,
- });
-
- const runSeerAnalysis = useRunSeerAnalysis({
- projectScore,
- projectData: projectData?.[0],
- transaction,
- webVitalTraceSamples,
});
- const runSeerAnalysisOnClickHandler = async () => {
- setIsCreatingIssues(true);
- const newIssueEventIds = await runSeerAnalysis();
- setNewlyCreatedIssueEventIds(newIssueEventIds);
- setIsCreatingIssues(false);
- };
-
- const hasProjectScore = !projectScoreIsLoading && Boolean(projectScore);
-
return (
@@ -220,12 +178,9 @@ export function PageOverviewSidebar({
{hasSeerWebVitalsSuggestions && (
)}
@@ -317,108 +272,75 @@ export function PageOverviewSidebar({
}
function SeerSuggestionsSection({
- isCreatingIssues,
- hasProjectScore,
- isLoadingWebVitalTraceSamples,
+ isLoading,
+ autofix,
issues,
- newlyCreatedIssueEventIds,
- runSeerAnalysis,
}: {
- hasProjectScore: boolean;
- isCreatingIssues: boolean;
- isLoadingWebVitalTraceSamples: boolean;
- issues: Group[] | undefined;
- newlyCreatedIssueEventIds: string[] | undefined;
- runSeerAnalysis: () => void;
+ isLoading: boolean;
+ autofix?: AutofixData[];
+ issues?: Group[];
}) {
- // Check if we are done loading issues.
- // If we created new issues in this session, we expect the issues results array to match in length. This should eventually be true, due to polling.
- // If we didn't create new issues, just check to see if the issues results array is defined.
- const areIssuesFullyLoaded =
- !!issues &&
- (newlyCreatedIssueEventIds
- ? issues.length === newlyCreatedIssueEventIds.length
- : true);
- const loading = !(
- areIssuesFullyLoaded &&
- hasProjectScore &&
- !isCreatingIssues &&
- !isLoadingWebVitalTraceSamples
- );
-
return (
-
-
- {t('Seer Suggestions')}
-
-
-
-
- {/* Issues are still loading, or projectScore is still loading, or seer analysis is still running */}
- {loading && }
- {/* Issues are done loading and they don't exist */}
- {!loading && issues.length === 0 && (
- }
- onClick={runSeerAnalysis}
- title={t(
- 'Create an issue for each underperforming web vital and run a root cause analysis.'
- )}
- >
- {t('Run Seer Analysis')}
-
- )}
- {/* Issues are done loading and they exist */}
- {!loading &&
- issues.length > 0 &&
- issues.map(issue => )}
-
-
-
+ !isLoading &&
+ autofix &&
+ autofix.length > 0 && (
+
+
+ {t('Seer Suggestions')}
+
+
+
+
+ {autofix &&
+ issues?.map((issue, index) => (
+
+ ))}
+
+
+
+ )
);
}
-function SeerSuggestion({issue}: {issue: Group}) {
+function SeerSuggestion({
+ issue,
+ autofix,
+ isLoading,
+}: {
+ autofix: AutofixData;
+ isLoading: boolean;
+ issue: Group;
+}) {
const organization = useOrganization();
- const {data, isLoading: isLoadingAutofix} = useApiQuery(
- makeAutofixQueryKey(organization.slug, issue.id),
- {
- staleTime: 0,
- refetchInterval: query => {
- const result = query.state.data?.[0];
- // Wait for any status other than PROCESSING to indicate the the run has stopped
- const isProcessing = result?.autofix?.status === 'PROCESSING';
- return !query.state.data?.[0]?.autofix || isProcessing ? POLL_INTERVAL : false;
- },
- }
- );
-
- const autofixData = data?.autofix;
const rootCauseDescription = useMemo(
- () => (autofixData ? getRootCauseDescription(autofixData) : null),
- [autofixData]
+ () => (autofix ? getRootCauseDescription(autofix) : null),
+ [autofix]
);
const rootCauseCopyText = useMemo(
- () => (autofixData ? getRootCauseCopyText(autofixData) : null),
- [autofixData]
+ () => (autofix ? getRootCauseCopyText(autofix) : null),
+ [autofix]
);
const solutionDescription = useMemo(
- () => (autofixData ? getSolutionDescription(autofixData) : null),
- [autofixData]
+ () => (autofix ? getSolutionDescription(autofix) : null),
+ [autofix]
);
const solutionCopyText = useMemo(
- () => (autofixData ? getSolutionCopyText(autofixData) : null),
- [autofixData]
+ () => (autofix ? getSolutionCopyText(autofix) : null),
+ [autofix]
);
const solutionIsLoading = useMemo(
- () => (autofixData ? getSolutionIsLoading(autofixData) : false),
- [autofixData]
+ () => (autofix ? getSolutionIsLoading(autofix) : false),
+ [autofix]
);
return (
@@ -431,9 +353,7 @@ function SeerSuggestion({issue}: {issue: Group}) {
- {isLoadingAutofix ||
- autofixData === undefined ||
- rootCauseDescription === null ? (
+ {isLoading || autofix === undefined || rootCauseDescription === null ? (
) : (
p.theme.space.xs};
padding-bottom: ${p => p.theme.space.xs};
`;
@@ -619,7 +539,3 @@ const StyledIconSeer = styled(IconSeer)`
const ViewIssueButtonContainer = styled('div')`
margin: ${p => p.theme.space.lg} 0;
`;
-
-const RunSeerAnalysisButton = styled(Button)`
- align-self: flex-start;
-`;
diff --git a/static/app/views/insights/browser/webVitals/queries/useSampleWebVitalTrace.tsx b/static/app/views/insights/browser/webVitals/queries/useSampleWebVitalTrace.tsx
deleted file mode 100644
index 10b3462f22a208..00000000000000
--- a/static/app/views/insights/browser/webVitals/queries/useSampleWebVitalTrace.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import {defined} from 'sentry/utils';
-import {decodeList} from 'sentry/utils/queryString';
-import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {useLocation} from 'sentry/utils/useLocation';
-import type {ProjectData} from 'sentry/views/insights/browser/webVitals/components/webVitalMeters';
-import {SPANS_FILTER} from 'sentry/views/insights/browser/webVitals/queries/useSpanSamplesWebVitalsQuery';
-import {Referrer} from 'sentry/views/insights/browser/webVitals/referrers';
-import type {WebVitals} from 'sentry/views/insights/browser/webVitals/types';
-import decodeBrowserTypes from 'sentry/views/insights/browser/webVitals/utils/queryParameterDecoders/browserType';
-import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';
-import {SpanFields, type SubregionCode} from 'sentry/views/insights/types';
-
-type Props = {
- transaction: string;
- webVital: WebVitals;
- enabled?: boolean;
- projectData?: ProjectData[];
-};
-
-function useSampleWebVitalTrace({
- transaction,
- projectData,
- webVital,
- enabled = true,
-}: Props) {
- const location = useLocation();
-
- let field: SpanFields | undefined;
- switch (webVital) {
- case 'cls':
- field = SpanFields.CLS;
- break;
- case 'fcp':
- field = SpanFields.FCP;
- break;
- case 'ttfb':
- field = SpanFields.TTFB;
- break;
- case 'inp':
- field = SpanFields.INP;
- break;
- case 'lcp':
- default:
- field = SpanFields.LCP;
- break;
- }
-
- const browserTypes = decodeBrowserTypes(location.query[SpanFields.BROWSER_NAME]);
- const subregions = decodeList(
- location.query[SpanFields.USER_GEO_SUBREGION]
- ) as SubregionCode[];
- const p75Value = projectData?.[0]?.[`p75(measurements.${webVital})`];
-
- const search = new MutableSearch(SPANS_FILTER);
- search.addFilterValue(SpanFields.TRANSACTION, transaction);
- if (defined(p75Value)) {
- search.addStringFilter(`measurements.${webVital}:>=${p75Value}`);
- }
- if (browserTypes) {
- search.addDisjunctionFilterValues(SpanFields.BROWSER_NAME, browserTypes);
- }
- if (subregions) {
- search.addDisjunctionFilterValues(SpanFields.USER_GEO_SUBREGION, subregions);
- }
-
- return useSpans(
- {
- search,
- sorts: [{field: `measurements.${webVital}`, kind: 'asc'}],
- fields: [SpanFields.TRACE, SpanFields.TIMESTAMP, field],
- enabled: defined(p75Value) && enabled,
- limit: 1, // We only need one sample to attach to the issue
- },
- Referrer.WEB_VITAL_SPANS
- );
-}
-
-// Unfortunately, we need to run separate queries for each web vital since we need a sample trace for each
-// Theres no way to get 5 samples for 5 separate web vital conditions in a single query at the moment
-export function useSampleWebVitalTraceParallel({
- transaction,
- projectData,
- enabled = true,
-}: Omit) {
- const {data: lcp, isLoading: isLcpLoading} = useSampleWebVitalTrace({
- transaction,
- projectData,
- webVital: 'lcp',
- enabled,
- });
- const {data: cls, isLoading: isClsLoading} = useSampleWebVitalTrace({
- transaction,
- projectData,
- webVital: 'cls',
- enabled,
- });
- const {data: fcp, isLoading: isFcpLoading} = useSampleWebVitalTrace({
- transaction,
- projectData,
- webVital: 'fcp',
- enabled,
- });
- const {data: ttfb, isLoading: isTtfbLoading} = useSampleWebVitalTrace({
- transaction,
- projectData,
- webVital: 'ttfb',
- enabled,
- });
- const {data: inp, isLoading: isInpLoading} = useSampleWebVitalTrace({
- transaction,
- projectData,
- webVital: 'inp',
- enabled,
- });
- const isLoading =
- isLcpLoading || isClsLoading || isFcpLoading || isTtfbLoading || isInpLoading;
-
- return {
- lcp: lcp?.[0],
- cls: cls?.[0],
- fcp: fcp?.[0],
- ttfb: ttfb?.[0],
- inp: inp?.[0],
- isLoading,
- };
-}
diff --git a/static/app/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery.tsx b/static/app/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery.tsx
index 1b3fdf7a1c0f15..c5a62a804d00f1 100644
--- a/static/app/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery.tsx
+++ b/static/app/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery.tsx
@@ -1,16 +1,12 @@
-import {useCallback} from 'react';
-
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import {IssueType, type Group, type ISSUE_TYPE_TO_ISSUE_TITLE} from 'sentry/types/group';
-import {useApiQuery, useQueryClient, type ApiQueryKey} from 'sentry/utils/queryClient';
+import {useApiQuery, type ApiQueryKey} from 'sentry/utils/queryClient';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import {ORDER} from 'sentry/views/insights/browser/webVitals/components/charts/performanceScoreChart';
import type {WebVitals} from 'sentry/views/insights/browser/webVitals/types';
-export const POLL_INTERVAL = 1000;
-
const DEFAULT_ISSUE_TYPES = [IssueType.WEB_VITALS];
type QueryProps = {
@@ -64,19 +60,6 @@ function useWebVitalsIssuesQueryKey({
];
}
-export function useInvalidateWebVitalsIssuesQuery({
- issueTypes = DEFAULT_ISSUE_TYPES,
- webVital,
- transaction,
-}: QueryProps) {
- const queryClient = useQueryClient();
- const queryKey = useWebVitalsIssuesQueryKey({issueTypes, transaction, webVital});
- return useCallback(() => {
- queryClient.setQueryData(queryKey, undefined);
- queryClient.invalidateQueries({queryKey});
- }, [queryClient, queryKey]);
-}
-
export function useWebVitalsIssuesQuery({
issueTypes = DEFAULT_ISSUE_TYPES,
webVital,
diff --git a/static/app/views/insights/browser/webVitals/utils/useCreateIssue.tsx b/static/app/views/insights/browser/webVitals/utils/useCreateIssue.tsx
deleted file mode 100644
index 000b902bd9a476..00000000000000
--- a/static/app/views/insights/browser/webVitals/utils/useCreateIssue.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import {fetchMutation, useMutation} from 'sentry/utils/queryClient';
-import type RequestError from 'sentry/utils/requestError/requestError';
-import useOrganization from 'sentry/utils/useOrganization';
-import usePageFilters from 'sentry/utils/usePageFilters';
-import useProjects from 'sentry/utils/useProjects';
-
-type CreateIssueData = Record;
-
-interface CreateIssueResponse {
- event_id: string;
-}
-
-export function useCreateIssue() {
- const organization = useOrganization();
- const {
- selection: {projects},
- } = usePageFilters();
- const {projects: allProjects} = useProjects();
-
- const orgSlug = organization.slug;
- const projectId = projects[0];
- const projectSlug =
- projectId === undefined
- ? null
- : allProjects.find(project => project.id === `${projectId}`)?.slug;
- return useMutation({
- mutationFn: (data: CreateIssueData) =>
- fetchMutation({
- url: `/projects/${orgSlug}/${projectSlug}/user-issue/`,
- method: 'POST',
- data,
- }),
- });
-}
diff --git a/static/app/views/insights/browser/webVitals/utils/useRunSeerAnalysis.tsx b/static/app/views/insights/browser/webVitals/utils/useRunSeerAnalysis.tsx
deleted file mode 100644
index 9c6f408c34b3dd..00000000000000
--- a/static/app/views/insights/browser/webVitals/utils/useRunSeerAnalysis.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import {useCallback} from 'react';
-
-import {IssueType} from 'sentry/types/group';
-import {ORDER} from 'sentry/views/insights/browser/webVitals/components/charts/performanceScoreChart';
-import type {ProjectData} from 'sentry/views/insights/browser/webVitals/components/webVitalMetersWithIssues';
-import {useInvalidateWebVitalsIssuesQuery} from 'sentry/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery';
-import type {ProjectScore} from 'sentry/views/insights/browser/webVitals/types';
-import {useCreateIssue} from 'sentry/views/insights/browser/webVitals/utils/useCreateIssue';
-import type {SpanFields, SpanResponse} from 'sentry/views/insights/types';
-
-type WebVitalTraceSample = Pick;
-
-type WebVitalTraceSamples = {
- cls?: WebVitalTraceSample;
- fcp?: WebVitalTraceSample;
- inp?: WebVitalTraceSample;
- lcp?: WebVitalTraceSample;
- ttfb?: WebVitalTraceSample;
-};
-
-// Creates a new issue for each web vital that has a score under 90 and runs seer autofix for each of them
-// TODO: Add logic to actually initiate running autofix for each issue. Right now we rely on the project config to automatically run autofix for each issue.
-export function useRunSeerAnalysis({
- projectScore,
- projectData,
- transaction,
- webVitalTraceSamples,
-}: {
- transaction: string;
- webVitalTraceSamples: WebVitalTraceSamples;
- projectData?: ProjectData;
- projectScore?: ProjectScore;
-}) {
- const {mutateAsync: createIssueAsync} = useCreateIssue();
- const invalidateWebVitalsIssuesQuery = useInvalidateWebVitalsIssuesQuery({
- transaction,
- });
-
- const runSeerAnalysis = useCallback(async (): Promise => {
- if (!projectScore || !projectData) {
- return [];
- }
- const underPerformingWebVitals = ORDER.filter(webVital => {
- const score = projectScore[`${webVital}Score`];
- return score && score < 90;
- });
- const promises = underPerformingWebVitals.map(async webVital => {
- try {
- const result = await createIssueAsync({
- issueType: IssueType.WEB_VITALS,
- vital: webVital,
- score: projectScore[`${webVital}Score`],
- value: Math.round(projectData[`p75(measurements.${webVital})`]),
- transaction,
- traceId: webVitalTraceSamples[webVital]?.trace,
- });
- return result.event_id;
- } catch (error) {
- // If the issue creation fails, we don't want to fail the entire operation for the rest of the vitals
- return null;
- }
- });
-
- const results = await Promise.all(promises);
- invalidateWebVitalsIssuesQuery();
- return results.filter(id => id !== null);
- }, [
- createIssueAsync,
- projectScore,
- projectData,
- transaction,
- invalidateWebVitalsIssuesQuery,
- webVitalTraceSamples,
- ]);
-
- return runSeerAnalysis;
-}
diff --git a/static/app/views/insights/browser/webVitals/utils/useSeerWebVitalsSuggestions.tsx b/static/app/views/insights/browser/webVitals/utils/useSeerWebVitalsSuggestions.tsx
new file mode 100644
index 00000000000000..79ac15573a9055
--- /dev/null
+++ b/static/app/views/insights/browser/webVitals/utils/useSeerWebVitalsSuggestions.tsx
@@ -0,0 +1,54 @@
+import {
+ makeAutofixQueryKey,
+ type AutofixResponse,
+} from 'sentry/components/events/autofix/useAutofix';
+import {IssueType} from 'sentry/types/group';
+import {fetchDataQuery, useQueries, type UseQueryResult} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useWebVitalsIssuesQuery} from 'sentry/views/insights/browser/webVitals/queries/useWebVitalsIssuesQuery';
+import {useHasSeerWebVitalsSuggestions} from 'sentry/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions';
+
+// Given a transaction name, fetches web vital issues + seer suggestions
+export function useSeerWebVitalsSuggestions({
+ transaction,
+ enabled = true,
+}: {
+ transaction: string;
+ enabled?: boolean;
+}) {
+ const hasSeerWebVitalsSuggestions = useHasSeerWebVitalsSuggestions();
+ const organization = useOrganization();
+
+ const {data: issues, isLoading} = useWebVitalsIssuesQuery({
+ issueTypes: [IssueType.WEB_VITALS],
+ transaction,
+ enabled: enabled && hasSeerWebVitalsSuggestions,
+ });
+
+ const autofixQueries: Array> = useQueries({
+ queries: (issues ?? []).map(issue => {
+ const queryKey = makeAutofixQueryKey(organization.slug, issue.id);
+ return {
+ queryKey,
+ queryFn: fetchDataQuery,
+ staleTime: Infinity,
+ enabled: !isLoading && enabled && hasSeerWebVitalsSuggestions,
+ retry: false,
+ };
+ }),
+ });
+
+ const isLoadingAutofix = autofixQueries.some(query => query.isPending);
+ const autofix = autofixQueries
+ .map(data => data.data?.[0]?.autofix ?? null)
+ .filter(data => data !== null);
+
+ return {
+ data: {
+ issues,
+ autofix:
+ autofix.length === 0 || autofix.length !== issues?.length ? undefined : autofix,
+ },
+ isLoading: isLoading || isLoadingAutofix,
+ };
+}
diff --git a/static/app/views/insights/browser/webVitals/views/pageOverview.tsx b/static/app/views/insights/browser/webVitals/views/pageOverview.tsx
index fd0563c9c7aee7..d5be8962d636e5 100644
--- a/static/app/views/insights/browser/webVitals/views/pageOverview.tsx
+++ b/static/app/views/insights/browser/webVitals/views/pageOverview.tsx
@@ -193,7 +193,6 @@ function PageOverview() {
projectScoreIsLoading={isPending}
browserTypes={browserTypes}
subregions={subregions}
- projectData={pageData}
/>