import {Fragment, useEffect} from 'react'; import styled from '@emotion/styled'; import type {LocationDescriptor} from 'history'; import omit from 'lodash/omit'; import Feature from 'sentry/components/acl/feature'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import Badge from 'sentry/components/badge/badge'; import FeatureBadge from 'sentry/components/badge/featureBadge'; import {Breadcrumbs} from 'sentry/components/breadcrumbs'; import Count from 'sentry/components/count'; import EventOrGroupTitle from 'sentry/components/eventOrGroupTitle'; import EventMessage from 'sentry/components/events/eventMessage'; import {GroupSummaryHeader} from 'sentry/components/group/groupSummary'; import {GroupStatusBadge} from 'sentry/components/group/inboxBadges/statusBadge'; import * as Layout from 'sentry/components/layouts/thirds'; import Link from 'sentry/components/links/link'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import ReplayCountBadge from 'sentry/components/replays/replayCountBadge'; import {TabList} from 'sentry/components/tabs'; import {IconChat} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import {IssueCategory, IssueType} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; import useReplayCountForIssues from 'sentry/utils/replayCount/useReplayCountForIssues'; import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay'; import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import GroupPriority from 'sentry/views/issueDetails/groupPriority'; import {useIssueDetailsHeader} from 'sentry/views/issueDetails/useIssueDetailsHeader'; import GroupActions from './actions'; import {Tab} from './types'; import type {ReprocessingStatus} from './utils'; type Props = { baseUrl: string; group: Group; groupReprocessingStatus: ReprocessingStatus; organization: Organization; project: Project; event?: Event; }; interface GroupHeaderTabsProps extends Pick { disabledTabs: Tab[]; eventRoute: LocationDescriptor; } export function GroupHeaderTabs({ baseUrl, disabledTabs, eventRoute, group, project, }: GroupHeaderTabsProps) { const organization = useOrganization(); const location = useLocation(); const {getReplayCountForIssue} = useReplayCountForIssues({ statsPeriod: '90d', }); const replaysCount = getReplayCountForIssue(group.id, group.issueCategory); // omit `sort` param from the URLs because it persists from the issues list, // which can cause the tab content rendering to break const queryParams = omit(location.query, ['sort']); const projectFeatures = new Set(project ? project.features : []); const organizationFeatures = new Set(organization ? organization.features : []); const hasSimilarView = projectFeatures.has('similarity-view'); const hasEventAttachments = organizationFeatures.has('event-attachments'); const hasReplaySupport = organizationFeatures.has('session-replay') && projectCanLinkToReplay(organization, project); const issueTypeConfig = getConfigForIssueType(group, project); useRouteAnalyticsParams({ group_has_replay: (replaysCount ?? 0) > 0, }); useEffect(() => { if (group.issueType === IssueType.REPLAY_HYDRATION_ERROR) { trackAnalytics('replay.hydration-error.issue-details-opened', {organization}); } }, [group.issueType, organization]); return ( {t('Details')} {t('Activity')} {group.numComments} ); } function GroupHeader({ baseUrl, group, groupReprocessingStatus, organization, event, project, }: Props) { const location = useLocation(); const { disabledTabs, message, eventRoute, disableActions, shortIdBreadcrumb, className, } = useIssueDetailsHeader({ group, groupReprocessingStatus, baseUrl, project, }); const {userCount} = group; const issueTypeConfig = getConfigForIssueType(group, project); const NEW_ISSUE_TYPES = [IssueType.REPLAY_HYDRATION_ERROR]; // adds a "new" banner next to the title return (
{NEW_ISSUE_TYPES.includes(group.issueType) && ( )}

{issueTypeConfig.stats.enabled && (
{t('Events')}
{t('Users')}
{userCount !== 0 ? ( ) : ( 0 )}
)}
{t('Priority')}
{/* Environment picker for mobile */}
); } export default GroupHeader; const BreadcrumbActionWrapper = styled('div')` display: flex; flex-direction: row; justify-content: space-between; gap: ${space(1)}; align-items: center; `; const HeaderRow = styled('div')` display: flex; gap: ${space(2)}; justify-content: space-between; margin-top: ${space(2)}; @media (max-width: ${p => p.theme.breakpoints.small}) { flex-direction: column; } `; const TitleWrapper = styled('div')` @media (min-width: ${p => p.theme.breakpoints.small}) { max-width: 65%; } `; const TitleHeading = styled('div')` display: flex; line-height: 2; gap: ${space(1)}; `; const StyledEventOrGroupTitle = styled(EventOrGroupTitle)` font-size: inherit; `; const StatsWrapper = styled('div')` display: flex; gap: calc(${space(3)} + ${space(3)}); @media (min-width: ${p => p.theme.breakpoints.small}) { justify-content: flex-end; } `; const IconBadge = styled(Badge)` display: flex; align-items: center; gap: ${space(0.5)}; `; const StyledTabList = styled(TabList)` margin-top: ${space(2)}; `; const PriorityContainer = styled('div')` /* Ensures that the layout doesn't shift when changing priority */ min-width: 80px; `; const StyledFeatureBadge = styled(FeatureBadge)` align-items: flex-start; `;