import styled from '@emotion/styled'; import omit from 'lodash/omit'; import {Button, LinkButton} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import {Chevron} from 'sentry/components/chevron'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {TabList, Tabs} from 'sentry/components/tabs'; import TimeSince from 'sentry/components/timeSince'; import {IconChevron, IconCopy} 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 {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import { getAnalyticsDataForEvent, getAnalyticsDataForGroup, getShortEventId, } from 'sentry/utils/events'; import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent'; import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; type EventNavigationProps = { event: Event; group: Group; }; type SectionDefinition = { condition: (event: Event) => boolean; label: string; section: string; }; enum EventNavOptions { RECOMMENDED = 'recommended', LATEST = 'latest', OLDEST = 'oldest', } const EventNavLabels = { [EventNavOptions.RECOMMENDED]: t('Recommended Event'), [EventNavOptions.OLDEST]: t('First Event'), [EventNavOptions.LATEST]: t('Last Event'), }; const eventDataSections: SectionDefinition[] = [ {section: 'event-highlights', label: t('Event Highlights'), condition: () => true}, { section: 'stacktrace', label: t('Stack Trace'), condition: (event: Event) => event.entries.some(entry => entry.type === 'stacktrace'), }, { section: 'exception', label: t('Exception'), condition: (event: Event) => event.entries.some(entry => entry.type === 'exception'), }, { section: 'breadcrumbs', label: t('Breadcrumbs'), condition: (event: Event) => event.entries.some(entry => entry.type === 'breadcrumbs'), }, {section: 'tags', label: t('Tags'), condition: (event: Event) => event.tags.length > 0}, {section: 'context', label: t('Context'), condition: (event: Event) => !!event.context}, { section: 'user-feedback', label: t('User Feedback'), condition: (event: Event) => !!event.userReport, }, { section: 'replay', label: t('Replay'), condition: (event: Event) => !!getReplayIdFromEvent(event), }, ]; export default function EventNavigation({event, group}: EventNavigationProps) { const location = useLocation(); const organization = useOrganization(); const hasPreviousEvent = defined(event.previousEventID); const hasNextEvent = defined(event.nextEventID); const baseEventsPath = `/organizations/${organization.slug}/issues/${group.id}/events/`; const jumpToSections = eventDataSections.filter(eventSection => eventSection.condition(event) ); const downloadJson = () => { const host = organization.links.regionUrl; const jsonUrl = `${host}/api/0/projects/${organization.slug}/${group.project.slug}/events/${event.id}/json/`; window.open(jsonUrl); trackAnalytics('issue_details.event_json_clicked', { organization, group_id: parseInt(`${event.groupID}`, 10), }); }; const {onClick: copyLink} = useCopyToClipboard({ successMessage: t('Event URL copied to clipboard'), text: window.location.origin + normalizeUrl(`${baseEventsPath}${event.id}/`), onCopy: () => trackAnalytics('issue_details.copy_event_link_clicked', { organization, ...getAnalyticsDataForGroup(group), ...getAnalyticsDataForEvent(event), }), }); const {onClick: copyEventId} = useCopyToClipboard({ successMessage: t('Event ID copied to clipboard'), text: event.id, }); return (
{Object.keys(EventNavLabels).map(label => { return ( {EventNavLabels[label]} ); })} } disabled={!hasPreviousEvent} to={{ pathname: `${baseEventsPath}${event.previousEventID}/`, query: {...location.query, referrer: 'previous-event'}, }} /> } disabled={!hasNextEvent} to={{ pathname: `${baseEventsPath}${event.nextEventID}/`, query: {...location.query, referrer: 'next-event'}, }} /> {t('View All Events')} {t('Event')} , size: 'zero', borderless: true, showChevron: false, }} position="bottom" size="xs" items={[ { key: 'copy-event-id', label: t('Copy Event ID'), onAction: copyEventId, }, { key: 'copy-event-link', label: t('Copy Event Link'), onAction: copyLink, }, { key: 'view-json', label: t('View JSON'), onAction: downloadJson, }, ]} />
{t('Jump to:')}
{jumpToSections.map(jump => ( { document .getElementById(jump.section) ?.scrollIntoView({behavior: 'smooth'}); }} borderless size="sm" > {jump.label} ))}
); } const EventNavigationWrapper = styled('div')` display: flex; justify-content: space-between; `; const NavigationWrapper = styled('div')` display: flex; `; const Navigation = styled('div')` display: flex; border-right: 1px solid ${p => p.theme.gray100}; `; const EventInfoJumpToWrapper = styled('div')` display: flex; gap: ${space(1)}; flex-direction: row; justify-content: space-between; align-items: center; `; const EventInfo = styled('div')` display: flex; gap: ${space(1)}; flex-direction: row; align-items: center; `; const JumpTo = styled('div')` display: flex; gap: ${space(1)}; flex-direction: row; align-items: center; color: ${p => p.theme.gray300}; `; const Divider = styled('hr')` height: 1px; width: 100%; background: ${p => p.theme.border}; border: none; margin-top: ${space(1)}; margin-bottom: ${space(1)}; `; const EventIdInfo = styled('span')` display: flex; align-items: center; gap: ${space(0.25)}; `; const EventId = styled('span')` position: relative; font-weight: ${p => p.theme.fontWeightBold}; font-size: ${p => p.theme.fontSizeLarge}; text-decoration: underline; text-decoration-color: ${p => p.theme.gray200}; &:hover { > span { display: flex; } } `; const StyledButton = styled(Button)` color: ${p => p.theme.gray300}; `; const CopyIconContainer = styled('span')` display: none; align-items: center; padding: ${space(0.25)}; background: ${p => p.theme.background}; position: absolute; right: 0; top: 50%; transform: translateY(-50%); `; const EventTitle = styled('div')` font-weight: ${p => p.theme.fontWeightBold}; font-size: ${p => p.theme.fontSizeLarge}; `;