import {browserHistory} from 'react-router';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import omit from 'lodash/omit';
import moment from 'moment-timezone';
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
import {Button, ButtonProps} from 'sentry/components/button';
import {CompactSelect} from 'sentry/components/compactSelect';
import DateTime from 'sentry/components/dateTime';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import FeatureBadge from 'sentry/components/featureBadge';
import {useOmniActions} from 'sentry/components/omniSearch/useOmniActions';
import TimeSince from 'sentry/components/timeSince';
import {Tooltip} from 'sentry/components/tooltip';
import {
IconChevron,
IconCopy,
IconEllipsis,
IconJson,
IconLink,
IconNext,
IconOpen,
IconPrevious,
IconStar,
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 {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
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 {useParams} from 'sentry/utils/useParams';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import EventCreatedTooltip from 'sentry/views/issueDetails/eventCreatedTooltip';
import {useDefaultIssueEvent} from 'sentry/views/issueDetails/utils';
import QuickTrace from './quickTrace';
type GroupEventCarouselProps = {
event: Event;
group: Group;
projectSlug: string;
};
type GroupEventNavigationProps = {
event: Event;
group: Group;
isDisabled: boolean;
};
type EventNavigationButtonProps = {
disabled: boolean;
group: Group;
icon: ButtonProps['icon'];
referrer: string;
title: string;
eventId?: string | null;
};
enum EventNavDropdownOption {
RECOMMENDED = 'recommended',
LATEST = 'latest',
OLDEST = 'oldest',
CUSTOM = 'custom',
ALL = 'all',
}
const BUTTON_SIZE = 'sm';
const BUTTON_ICON_SIZE = 'sm';
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 (
);
}
function EventNavigationDropdown({group, event, isDisabled}: GroupEventNavigationProps) {
const location = useLocation();
const params = useParams<{eventId?: string}>();
const theme = useTheme();
const organization = useOrganization();
const largeViewport = useMedia(`(min-width: ${theme.breakpoints.large})`);
const defaultIssueEvent = useDefaultIssueEvent();
const isHelpfulEventUiEnabled =
organization.features.includes('issue-details-most-helpful-event') &&
organization.features.includes('issue-details-most-helpful-event-ui');
if (!isHelpfulEventUiEnabled || !largeViewport) {
return null;
}
const getSelectedOption = () => {
switch (params.eventId) {
case EventNavDropdownOption.RECOMMENDED:
case EventNavDropdownOption.LATEST:
case EventNavDropdownOption.OLDEST:
return params.eventId;
case undefined:
return defaultIssueEvent;
default:
return undefined;
}
};
const selectedValue = getSelectedOption();
const eventNavDropdownOptions = [
{
value: EventNavDropdownOption.RECOMMENDED,
label: (
{t('Recommended')}
),
textValue: t('Recommended'),
details: t('Event with the most context'),
},
{
value: EventNavDropdownOption.LATEST,
label: t('Latest'),
details: t('Last seen event in this issue'),
},
{
value: EventNavDropdownOption.OLDEST,
label: t('Oldest'),
details: t('First seen event in this issue'),
},
...(!selectedValue
? [
{
value: EventNavDropdownOption.CUSTOM,
label: t('Custom Selection'),
},
]
: []),
{
options: [{value: EventNavDropdownOption.ALL, label: 'View All Events'}],
},
];
return (
) : selectedValue === EventNavDropdownOption.RECOMMENDED ? (
t('Recommended')
) : undefined
}
menuWidth={232}
onChange={selectedOption => {
trackAnalytics('issue_details.event_dropdown_option_selected', {
organization,
selected_event_type: selectedOption.value,
from_event_type: selectedValue ?? EventNavDropdownOption.CUSTOM,
event_id: event.id,
group_id: group.id,
});
switch (selectedOption.value) {
case EventNavDropdownOption.RECOMMENDED:
case EventNavDropdownOption.LATEST:
case EventNavDropdownOption.OLDEST:
browserHistory.push({
pathname: normalizeUrl(
makeBaseEventsPath({organization, group}) + selectedOption.value + '/'
),
query: {...location.query, referrer: `${selectedOption.value}-event`},
});
break;
case EventNavDropdownOption.ALL:
const searchTermWithoutQuery = omit(location.query, 'query');
browserHistory.push({
pathname: normalizeUrl(
`/organizations/${organization.slug}/issues/${group.id}/events/`
),
query: searchTermWithoutQuery,
});
break;
default:
break;
}
}}
/>
);
}
export function GroupEventCarousel({event, group, projectSlug}: GroupEventCarouselProps) {
const theme = useTheme();
const organization = useOrganization();
const location = useLocation();
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') &&
projectCanLinkToReplay(group.project);
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 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 {onClick: copyLink} = useCopyToClipboard({
successMessage: t('Event URL copied to clipboard'),
text:
window.location.origin +
normalizeUrl(`${makeBaseEventsPath({organization, group})}${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,
});
const isHelpfulEventUiEnabled =
organization.features.includes('issue-details-most-helpful-event') &&
organization.features.includes('issue-details-most-helpful-event-ui');
useOmniActions([
{
key: 'issue-next-event',
label: t('Go to Recommended Event'),
areaKey: 'issue',
actionType: 'navigate',
actionIcon: IconStar,
to: normalizeUrl(
makeBaseEventsPath({organization, group}) +
EventNavDropdownOption.RECOMMENDED +
'/'
),
},
{
key: 'issue-oldest-event',
label: t('Go to Oldest Event'),
areaKey: 'issue',
actionType: 'navigate',
actionIcon: IconPrevious,
actionHotkey: 'shift+[',
to: normalizeUrl(
makeBaseEventsPath({organization, group}) + EventNavDropdownOption.OLDEST + '/'
),
},
{
key: 'issue-latest-event',
label: t('Go to Latest Event'),
areaKey: 'issue',
actionType: 'navigate',
actionIcon: IconNext,
actionHotkey: 'shift+]',
to: normalizeUrl(
makeBaseEventsPath({organization, group}) + EventNavDropdownOption.LATEST + '/'
),
},
]);
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: copyEventId,
},
{
key: 'copy-event-url',
label: t('Copy Event Link'),
hidden: xlargeViewport,
onAction: copyLink,
},
{
key: 'json',
label: `JSON (${formatBytesBase2(event.size)})`,
onAction: downloadJson,
hidden: xlargeViewport,
},
{
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 && (
: undefined}
>
{!isHelpfulEventUiEnabled && 'Copy Link'}
)}
{xlargeViewport && (
) : (
)
}
>
{!isHelpfulEventUiEnabled && 'JSON'}
)}
{!isHelpfulEventUiEnabled && (
}
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"
/>
{!isHelpfulEventUiEnabled && (
}
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')`
display: flex;
align-items: center;
flex-wrap: wrap;
gap: ${space(1)};
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 EventIdAndTimeContainer = styled('div')`
display: flex;
align-items: center;
column-gap: ${space(0.75)};
row-gap: 0;
flex-wrap: wrap;
`;
const EventIdContainer = styled('div')`
display: flex;
align-items: center;
column-gap: ${space(0.25)};
`;
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;
}
}
@media (max-width: 600px) {
font-size: ${p => p.theme.fontSizeMedium};
}
`;
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%);
`;