import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import moment from 'moment-timezone'; import {addSuccessMessage} from 'sentry/actionCreators/indicator'; import {Button, ButtonProps} from 'sentry/components/button'; import DateTime from 'sentry/components/dateTime'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {Tooltip} from 'sentry/components/tooltip'; import { IconChevron, IconCopy, IconEllipsis, IconNext, IconOpen, IconPrevious, IconWarning, } from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {Event, Group, Organization} from 'sentry/types'; import {defined, formatBytesBase2} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {eventDetailsRoute, generateEventSlug} from 'sentry/utils/discover/urls'; import { getAnalyticsDataForEvent, getAnalyticsDataForGroup, getShortEventId, } from 'sentry/utils/events'; import getDynamicText from 'sentry/utils/getDynamicText'; import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; import useOrganization from 'sentry/utils/useOrganization'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import EventCreatedTooltip from 'sentry/views/issueDetails/eventCreatedTooltip'; import QuickTrace from './quickTrace'; type GroupEventCarouselProps = { event: Event; group: Group; projectSlug: string; }; type EventNavigationButtonProps = { disabled: boolean; group: Group; icon: ButtonProps['icon']; referrer: string; title: string; eventId?: string | null; }; const BUTTON_SIZE = 'sm'; const BUTTON_ICON_SIZE = 'sm'; const copyToClipboard = (value: string) => { navigator.clipboard .writeText(value) .then(() => { addSuccessMessage(t('Copied to clipboard')); }) .catch(() => { t('Error copying to clipboard'); }); }; const makeBaseEventsPath = ({ organization, group, }: { group: Group; organization: Organization; }) => `/organizations/${organization.slug}/issues/${group.id}/events/`; function EventNavigationButton({ disabled, eventId, group, icon, title, referrer, }: EventNavigationButtonProps) { const organization = useOrganization(); const location = useLocation(); const baseEventsPath = makeBaseEventsPath({organization, group}); // Need to wrap with Tooltip because our version of React Router doesn't allow access // to the anchor ref which is needed by Tooltip to position correctly. return (
); } export function GroupEventCarousel({event, group, projectSlug}: GroupEventCarouselProps) { const theme = useTheme(); const organization = useOrganization(); const location = useLocation(); const largeViewport = useMedia(`(min-width: ${theme.breakpoints.large})`); const xlargeViewport = useMedia(`(min-width: ${theme.breakpoints.xlarge})`); const hasReplay = Boolean(event?.tags?.find(({key}) => key === 'replayId')?.value); const isReplayEnabled = organization.features.includes('session-replay'); const latencyThreshold = 30 * 60 * 1000; // 30 minutes const isOverLatencyThreshold = event.dateReceived && event.dateCreated && Math.abs(+moment(event.dateReceived) - +moment(event.dateCreated)) > latencyThreshold; const hasPreviousEvent = defined(event.previousEventID); const hasNextEvent = defined(event.nextEventID); const {onClick: onClickCopy} = useCopyToClipboard({text: event.id}); const downloadJson = () => { const jsonUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${event.id}/json/`; window.open(jsonUrl); trackAnalytics('issue_details.event_json_clicked', { organization, group_id: parseInt(`${event.groupID}`, 10), }); }; const copyLink = () => { copyToClipboard( window.location.origin + normalizeUrl(`${makeBaseEventsPath({organization, group})}${event.id}/`) ); trackAnalytics('issue_details.copy_event_link_clicked', { organization, ...getAnalyticsDataForGroup(group), ...getAnalyticsDataForEvent(event), }); }; return ( Event ID:{' '} {' '} {(event.dateCreated ?? event.dateReceived) && ( {getDynamicText({ fixed: 'Jan 1, 12:00 AM', value: ( } overlayStyle={{maxWidth: 300}} > ), })} {isOverLatencyThreshold && ( )} )} , showChevron: false, size: BUTTON_SIZE, }} items={[ { key: 'copy-event-id', label: t('Copy Event ID'), onAction: () => copyToClipboard(event.id), }, { key: 'copy-event-url', label: t('Copy Event Link'), hidden: xlargeViewport, onAction: copyLink, }, { key: 'json', label: `JSON (${formatBytesBase2(event.size)})`, onAction: downloadJson, hidden: largeViewport, }, { key: 'full-event-discover', label: t('Full Event Details'), hidden: !organization.features.includes('discover-basic'), to: eventDetailsRoute({ eventSlug: generateEventSlug({project: projectSlug, id: event.id}), orgSlug: organization.slug, }), onAction: () => { trackAnalytics('issue_details.event_details_clicked', { organization, ...getAnalyticsDataForGroup(group), ...getAnalyticsDataForEvent(event), }); }, }, { key: 'replay', label: t('View Replay'), hidden: !hasReplay || !isReplayEnabled, onAction: () => { const breadcrumbsHeader = document.getElementById('breadcrumbs'); if (breadcrumbsHeader) { breadcrumbsHeader.scrollIntoView({behavior: 'smooth'}); } trackAnalytics('issue_details.header_view_replay_clicked', { organization, ...getAnalyticsDataForGroup(group), ...getAnalyticsDataForEvent(event), }); }, }, ]} /> {xlargeViewport && ( )} {largeViewport && ( )} } disabled={!hasPreviousEvent} title={t('First Event')} eventId="oldest" referrer="oldest-event" /> } disabled={!hasPreviousEvent} title={t('Previous Event')} eventId={event.previousEventID} referrer="previous-event" /> } disabled={!hasNextEvent} title={t('Next Event')} eventId={event.nextEventID} referrer="next-event" /> } disabled={!hasNextEvent} title={t('Latest Event')} eventId="latest" referrer="latest-event" /> ); } const CarouselAndButtonsWrapper = styled('div')` display: flex; justify-content: space-between; align-items: flex-start; gap: ${space(1)}; margin-bottom: ${space(0.5)}; `; const EventHeading = styled('div')` font-size: ${p => p.theme.fontSizeLarge}; @media (max-width: 600px) { font-size: ${p => p.theme.fontSizeMedium}; } `; const ActionsWrapper = styled('div')` display: flex; align-items: center; gap: ${space(0.5)}; `; const StyledNavButton = styled(Button)` border-radius: 0; `; const NavButtons = styled('div')` display: flex; > * { &:not(:last-child) { ${StyledNavButton} { border-right: none; } } &:first-child { ${StyledNavButton} { border-radius: ${p => p.theme.borderRadius} 0 0 ${p => p.theme.borderRadius}; } } &:last-child { ${StyledNavButton} { border-radius: 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0; } } } `; const EventIdLabel = styled('span')` font-weight: bold; `; const EventTimeLabel = styled('span')` color: ${p => p.theme.subText}; `; const StyledIconWarning = styled(IconWarning)` margin-left: ${space(0.25)}; position: relative; top: 1px; `; const EventId = styled('span')` position: relative; font-weight: normal; font-size: ${p => p.theme.fontSizeLarge}; &:hover { > span { display: flex; } } `; 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%); `;