From e2dc1dad0582df4c97664d15c985b30052adcc8e Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 17 Nov 2025 12:56:39 -0500 Subject: [PATCH 1/6] refactor seer sidebar component for webvitals --- .../components/pageOverviewSidebar.tsx | 200 +++++------------- .../utils/useSeerWebVitalsSuggestions.tsx | 54 +++++ .../browser/webVitals/views/pageOverview.tsx | 1 - 3 files changed, 112 insertions(+), 143 deletions(-) create mode 100644 static/app/views/insights/browser/webVitals/utils/useSeerWebVitalsSuggestions.tsx diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx index cd7e7017f01e27..a06dfcf1d63448 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.lg} 0; `; - -const RunSeerAnalysisButton = styled(Button)` - align-self: flex-start; -`; 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} /> From 8a6b8e2c404fdfe51049f0cbe6e68bc80deb8179 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 17 Nov 2025 13:18:44 -0500 Subject: [PATCH 2/6] fix test --- .../components/pageOverviewSidebar.spec.tsx | 18 ------------------ 1 file changed, 18 deletions(-) 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..3f23921c91465a 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx @@ -228,15 +228,6 @@ describe('PageOverviewSidebar', () => { , {organization} ); @@ -274,15 +265,6 @@ describe('PageOverviewSidebar', () => { ttfbScore: 100, inpScore: 80, }} - projectData={[ - { - 'p75(measurements.lcp)': 2500, - 'p75(measurements.cls)': 0.1, - 'p75(measurements.fcp)': 1800, - 'p75(measurements.ttfb)': 600, - 'p75(measurements.inp)': 200, - }, - ]} />, {organization} ); From 1d6783acdd6345e0170e89a848588515c211566f Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 17 Nov 2025 15:41:16 -0500 Subject: [PATCH 3/6] knip --- .../queries/useSampleWebVitalTrace.tsx | 126 ------------------ .../queries/useWebVitalsIssuesQuery.tsx | 19 +-- .../webVitals/utils/useCreateIssue.tsx | 34 ----- .../webVitals/utils/useRunSeerAnalysis.tsx | 77 ----------- 4 files changed, 1 insertion(+), 255 deletions(-) delete mode 100644 static/app/views/insights/browser/webVitals/queries/useSampleWebVitalTrace.tsx delete mode 100644 static/app/views/insights/browser/webVitals/utils/useCreateIssue.tsx delete mode 100644 static/app/views/insights/browser/webVitals/utils/useRunSeerAnalysis.tsx 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; -} From b903dadb743dc6db78667db5a09be377db8d4992 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 17 Nov 2025 16:35:29 -0500 Subject: [PATCH 4/6] unused tests --- .../components/pageOverviewSidebar.spec.tsx | 91 +------------------ 1 file changed, 1 insertion(+), 90 deletions(-) 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 3f23921c91465a..2035a9ea3d6040 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; @@ -218,92 +216,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(); - }); }); }); From 145009f3934fe6e2f8353411f1bd107e490681c8 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Mon, 17 Nov 2025 17:04:30 -0500 Subject: [PATCH 5/6] unused mocks --- .../components/pageOverviewSidebar.spec.tsx | 13 ------------- 1 file changed, 13 deletions(-) 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 2035a9ea3d6040..fdcd0205d9601f 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.spec.tsx @@ -51,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: [ @@ -87,12 +80,6 @@ describe('PageOverviewSidebar', () => { }, }, }); - - userIssueMock = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/user-issue/`, - body: {event_id: '123'}, - method: 'POST', - }); }); afterEach(() => { From a3b92add3c7a75bf838c5f5f23f93546ce900903 Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Tue, 18 Nov 2025 13:08:42 -0500 Subject: [PATCH 6/6] fix --- .../browser/webVitals/components/pageOverviewSidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx index a06dfcf1d63448..2c77cd32461cf0 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewSidebar.tsx @@ -284,7 +284,7 @@ function SeerSuggestionsSection({ !isLoading && autofix && autofix.length > 0 && ( -
+ {t('Seer Suggestions')} @@ -302,7 +302,7 @@ function SeerSuggestionsSection({ ))} -
+
) ); } @@ -495,7 +495,7 @@ const SeerSuggestionCard = styled('div')` const CardTitle = styled('div')` display: flex; - align-items: center; + align-items: flex-start; gap: ${p => p.theme.space.xs}; padding-bottom: ${p => p.theme.space.xs}; `;