mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
import {useMemo, useState} from 'react';
|
|
import {browserHistory} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import omit from 'lodash/omit';
|
|
import moment from 'moment';
|
|
|
|
import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
|
|
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
|
|
import {LinkButton} from 'sentry/components/button';
|
|
import {AggregateSpans} from 'sentry/components/events/interfaces/spans/aggregateSpans';
|
|
import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget';
|
|
import * as Layout from 'sentry/components/layouts/thirds';
|
|
import ExternalLink from 'sentry/components/links/externalLink';
|
|
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
|
|
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
|
|
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
|
|
import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
|
|
import {TabList, Tabs} from 'sentry/components/tabs';
|
|
import {IconChevron, IconClose} from 'sentry/icons';
|
|
import {t, tct} from 'sentry/locale';
|
|
import ConfigStore from 'sentry/stores/configStore';
|
|
import {space} from 'sentry/styles/space';
|
|
import {defined} from 'sentry/utils';
|
|
import {decodeScalar} from 'sentry/utils/queryString';
|
|
import useDismissAlert from 'sentry/utils/useDismissAlert';
|
|
import {useLocation} from 'sentry/utils/useLocation';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
import useRouter from 'sentry/utils/useRouter';
|
|
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
|
|
import {PageOverviewSidebar} from 'sentry/views/performance/browser/webVitals/components/pageOverviewSidebar';
|
|
import {
|
|
PerformanceScoreBreakdownChart,
|
|
SCORE_MIGRATION_TIMESTAMP,
|
|
} from 'sentry/views/performance/browser/webVitals/components/performanceScoreBreakdownChart';
|
|
import WebVitalMeters from 'sentry/views/performance/browser/webVitals/components/webVitalMeters';
|
|
import {PageOverviewWebVitalsDetailPanel} from 'sentry/views/performance/browser/webVitals/pageOverviewWebVitalsDetailPanel';
|
|
import {PageSamplePerformanceTable} from 'sentry/views/performance/browser/webVitals/pageSamplePerformanceTable';
|
|
import {calculatePerformanceScoreFromTableDataRow} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/calculatePerformanceScore';
|
|
import {useProjectRawWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/useProjectRawWebVitalsQuery';
|
|
import {calculatePerformanceScoreFromStoredTableDataRow} from 'sentry/views/performance/browser/webVitals/utils/queries/storedScoreQueries/calculatePerformanceScoreFromStored';
|
|
import {useProjectWebVitalsScoresQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/storedScoreQueries/useProjectWebVitalsScoresQuery';
|
|
import type {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types';
|
|
import {useStoredScoresSetting} from 'sentry/views/performance/browser/webVitals/utils/useStoredScoresSetting';
|
|
import {
|
|
AlertContent,
|
|
DismissButton,
|
|
StyledAlert,
|
|
} from 'sentry/views/performance/browser/webVitals/webVitalsLandingPage';
|
|
import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
|
|
|
|
import {transactionSummaryRouteWithQuery} from '../../transactionSummary/utils';
|
|
|
|
export enum LandingDisplayField {
|
|
OVERVIEW = 'overview',
|
|
SPANS = 'spans',
|
|
}
|
|
|
|
const LANDING_DISPLAYS = [
|
|
{
|
|
label: t('Overview'),
|
|
field: LandingDisplayField.OVERVIEW,
|
|
},
|
|
{
|
|
label: t('Aggregate Spans'),
|
|
field: LandingDisplayField.SPANS,
|
|
},
|
|
];
|
|
|
|
function getCurrentTabSelection(selectedTab) {
|
|
const tab = decodeScalar(selectedTab);
|
|
if (tab && Object.values(LandingDisplayField).includes(tab as LandingDisplayField)) {
|
|
return tab as LandingDisplayField;
|
|
}
|
|
return LandingDisplayField.OVERVIEW;
|
|
}
|
|
|
|
export default function PageOverview() {
|
|
const organization = useOrganization();
|
|
const location = useLocation();
|
|
const {projects} = useProjects();
|
|
const router = useRouter();
|
|
const shouldUseStoredScores = useStoredScoresSetting();
|
|
const transaction = location.query.transaction
|
|
? Array.isArray(location.query.transaction)
|
|
? location.query.transaction[0]
|
|
: location.query.transaction
|
|
: undefined;
|
|
const project = useMemo(
|
|
() => projects.find(p => p.id === String(location.query.project)),
|
|
[projects, location.query.project]
|
|
);
|
|
|
|
const tab = getCurrentTabSelection(location.query.tab);
|
|
|
|
// TODO: When visiting page overview from a specific webvital detail panel in the landing page,
|
|
// we should automatically default this webvital state to the respective webvital so the detail
|
|
// panel in this page opens automatically.
|
|
const [state, setState] = useState<{webVital: WebVitals | null}>({
|
|
webVital: (location.query.webVital as WebVitals) ?? null,
|
|
});
|
|
|
|
const user = ConfigStore.get('user');
|
|
|
|
const {dismiss, isDismissed} = useDismissAlert({
|
|
key: `${organization.slug}-${user.id}:performance-score-migration-message-dismissed`,
|
|
});
|
|
|
|
const query = decodeScalar(location.query.query);
|
|
|
|
const {data: pageData, isLoading} = useProjectRawWebVitalsQuery({transaction});
|
|
const {data: projectScores, isLoading: isProjectScoresLoading} =
|
|
useProjectWebVitalsScoresQuery({transaction, enabled: shouldUseStoredScores});
|
|
|
|
if (transaction === undefined) {
|
|
// redirect user to webvitals landing page
|
|
window.location.href = normalizeUrl(
|
|
`/organizations/${organization.slug}/performance/browser/pageloads/`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const transactionSummaryTarget =
|
|
project &&
|
|
!Array.isArray(location.query.project) && // Only render button to transaction summary when one project is selected.
|
|
transaction &&
|
|
transactionSummaryRouteWithQuery({
|
|
orgSlug: organization.slug,
|
|
transaction,
|
|
query: {...location.query},
|
|
projectID: project.id,
|
|
});
|
|
|
|
const projectScore =
|
|
(shouldUseStoredScores && isProjectScoresLoading) || isLoading
|
|
? undefined
|
|
: shouldUseStoredScores
|
|
? calculatePerformanceScoreFromStoredTableDataRow(projectScores?.data?.[0])
|
|
: calculatePerformanceScoreFromTableDataRow(pageData?.data?.[0]);
|
|
|
|
const scoreMigrationTimestampString = moment(SCORE_MIGRATION_TIMESTAMP).format(
|
|
'DD MMMM YYYY'
|
|
);
|
|
|
|
return (
|
|
<ModulePageProviders title={[t('Performance'), t('Web Vitals')].join(' — ')}>
|
|
<Tabs
|
|
value={tab}
|
|
onChange={value => {
|
|
browserHistory.push({
|
|
...location,
|
|
query: {
|
|
...location.query,
|
|
tab: value,
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<Layout.Header>
|
|
<Layout.HeaderContent>
|
|
<Breadcrumbs
|
|
crumbs={[
|
|
{
|
|
label: 'Performance',
|
|
to: normalizeUrl(`/organizations/${organization.slug}/performance/`),
|
|
preservePageFilters: true,
|
|
},
|
|
{
|
|
label: 'Web Vitals',
|
|
to: normalizeUrl(
|
|
`/organizations/${organization.slug}/performance/browser/pageloads/`
|
|
),
|
|
preservePageFilters: true,
|
|
},
|
|
...(transaction ? [{label: 'Page Overview'}] : []),
|
|
]}
|
|
/>
|
|
<Layout.Title>
|
|
{transaction && project && <ProjectAvatar project={project} size={24} />}
|
|
{transaction ?? t('Page Loads')}
|
|
</Layout.Title>
|
|
</Layout.HeaderContent>
|
|
<Layout.HeaderActions>
|
|
{transactionSummaryTarget && (
|
|
<LinkButton to={transactionSummaryTarget} size="sm">
|
|
{t('View Transaction Summary')}
|
|
</LinkButton>
|
|
)}
|
|
</Layout.HeaderActions>
|
|
<TabList hideBorder>
|
|
{LANDING_DISPLAYS.map(({label, field}) => (
|
|
<TabList.Item key={field}>{label}</TabList.Item>
|
|
))}
|
|
</TabList>
|
|
</Layout.Header>
|
|
{tab === LandingDisplayField.SPANS ? (
|
|
<Layout.Body>
|
|
<Layout.Main fullWidth>
|
|
{defined(transaction) && <AggregateSpans transaction={transaction} />}
|
|
</Layout.Main>
|
|
</Layout.Body>
|
|
) : (
|
|
<Layout.Body>
|
|
<FloatingFeedbackWidget />
|
|
<Layout.Main>
|
|
<TopMenuContainer>
|
|
{transaction && (
|
|
<ViewAllPagesButton
|
|
to={{
|
|
...location,
|
|
pathname: '/performance/browser/pageloads/',
|
|
query: {...location.query, transaction: undefined},
|
|
}}
|
|
>
|
|
<IconChevron direction="left" /> {t('View All Pages')}
|
|
</ViewAllPagesButton>
|
|
)}
|
|
<PageFilterBar condensed>
|
|
<ProjectPageFilter />
|
|
<EnvironmentPageFilter />
|
|
<DatePageFilter />
|
|
</PageFilterBar>
|
|
</TopMenuContainer>
|
|
{shouldUseStoredScores && !isDismissed && (
|
|
<StyledAlert type="info" showIcon>
|
|
<AlertContent>
|
|
<span>
|
|
{tct(
|
|
`We made improvements to how Performance Scores are calculated for your projects. Starting on [scoreMigrationTimestampString], scores are updated to more accurately reflect user experiences. [link:Read more about these improvements].`,
|
|
{
|
|
scoreMigrationTimestampString,
|
|
link: (
|
|
<ExternalLink href="https://sentry.engineering/blog/how-we-improved-performance-score-accuracy" />
|
|
),
|
|
}
|
|
)}
|
|
</span>
|
|
<DismissButton
|
|
priority="link"
|
|
icon={<IconClose />}
|
|
onClick={dismiss}
|
|
aria-label={t('Dismiss Alert')}
|
|
title={t('Dismiss Alert')}
|
|
/>
|
|
</AlertContent>
|
|
</StyledAlert>
|
|
)}
|
|
<Flex>
|
|
<PerformanceScoreBreakdownChart transaction={transaction} />
|
|
</Flex>
|
|
<WebVitalMetersContainer>
|
|
<WebVitalMeters
|
|
projectData={pageData}
|
|
projectScore={projectScore}
|
|
onClick={webVital => {
|
|
router.replace({
|
|
pathname: location.pathname,
|
|
query: {...location.query, webVital},
|
|
});
|
|
setState({...state, webVital});
|
|
}}
|
|
transaction={transaction}
|
|
showTooltip={false}
|
|
/>
|
|
</WebVitalMetersContainer>
|
|
<PageSamplePerformanceTableContainer>
|
|
<PageSamplePerformanceTable
|
|
transaction={transaction}
|
|
limit={15}
|
|
search={query}
|
|
/>
|
|
</PageSamplePerformanceTableContainer>
|
|
</Layout.Main>
|
|
<Layout.Side>
|
|
<PageOverviewSidebar
|
|
projectScore={projectScore}
|
|
transaction={transaction}
|
|
projectScoreIsLoading={isLoading}
|
|
/>
|
|
</Layout.Side>
|
|
</Layout.Body>
|
|
)}
|
|
<PageOverviewWebVitalsDetailPanel
|
|
webVital={state.webVital}
|
|
onClose={() => {
|
|
router.replace({
|
|
pathname: router.location.pathname,
|
|
query: omit(router.location.query, 'webVital'),
|
|
});
|
|
setState({...state, webVital: null});
|
|
}}
|
|
/>
|
|
</Tabs>
|
|
</ModulePageProviders>
|
|
);
|
|
}
|
|
|
|
const ViewAllPagesButton = styled(LinkButton)`
|
|
margin-right: ${space(1)};
|
|
`;
|
|
|
|
const TopMenuContainer = styled('div')`
|
|
margin-bottom: ${space(1)};
|
|
display: flex;
|
|
`;
|
|
|
|
const Flex = styled('div')`
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
gap: ${space(1)};
|
|
`;
|
|
|
|
const PageSamplePerformanceTableContainer = styled('div')`
|
|
margin-top: ${space(1)};
|
|
`;
|
|
|
|
const WebVitalMetersContainer = styled('div')`
|
|
margin: ${space(2)} 0 ${space(4)} 0;
|
|
`;
|