import {Component, Fragment} from 'react';
import {Location, LocationDescriptor} from 'history';
import DropdownLink from 'sentry/components/dropdownLink';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import {
ErrorDestination,
generateSingleErrorTarget,
generateSingleTransactionTarget,
generateTraceTarget,
isQuickTraceEvent,
TransactionDestination,
} from 'sentry/components/quickTrace/utils';
import Tooltip from 'sentry/components/tooltip';
import {backend, frontend, mobile, serverless} from 'sentry/data/platformCategories';
import {IconFire} from 'sentry/icons';
import {t, tct, tn} from 'sentry/locale';
import {OrganizationSummary} from 'sentry/types';
import {Event} from 'sentry/types/event';
import {trackAnalyticsEvent} from 'sentry/utils/analytics';
import {getDocsPlatform} from 'sentry/utils/docs';
import {getDuration} from 'sentry/utils/formatters';
import localStorage from 'sentry/utils/localStorage';
import {
QuickTrace as QuickTraceType,
QuickTraceEvent,
TraceError,
} from 'sentry/utils/performance/quickTrace/types';
import {parseQuickTrace} from 'sentry/utils/performance/quickTrace/utils';
import Projects from 'sentry/utils/projects';
import {Theme} from 'sentry/utils/theme';
const FRONTEND_PLATFORMS: string[] = [...frontend, ...mobile];
const BACKEND_PLATFORMS: string[] = [...backend, ...serverless];
import {
DropdownContainer,
DropdownItem,
DropdownItemSubContainer,
DropdownMenuHeader,
ErrorNodeContent,
EventNode,
ExternalDropdownLink,
QuickTraceContainer,
QuickTraceValue,
SectionSubtext,
SingleEventHoverText,
TraceConnector,
} from './styles';
const TOOLTIP_PREFIX = {
root: 'root',
ancestors: 'ancestor',
parent: 'parent',
current: '',
children: 'child',
descendants: 'descendant',
};
type QuickTraceProps = Pick<
EventNodeSelectorProps,
'anchor' | 'errorDest' | 'transactionDest'
> & {
event: Event;
location: Location;
organization: OrganizationSummary;
quickTrace: QuickTraceType;
};
export default function QuickTrace({
event,
quickTrace,
location,
organization,
anchor,
errorDest,
transactionDest,
}: QuickTraceProps) {
let parsedQuickTrace;
try {
parsedQuickTrace = parseQuickTrace(quickTrace, event, organization);
} catch (error) {
return {'\u2014'};
}
const traceLength = quickTrace.trace && quickTrace.trace.length;
const {root, ancestors, parent, children, descendants, current} = parsedQuickTrace;
const nodes: React.ReactNode[] = [];
if (root) {
nodes.push(
);
nodes.push();
}
if (ancestors?.length) {
nodes.push(
);
nodes.push();
}
if (parent) {
nodes.push(
);
nodes.push();
}
const currentNode = (
);
if (traceLength === 1) {
nodes.push(
{({projects}) => {
const project = projects.find(p => p.slug === current.project_slug);
if (project?.platform) {
if (BACKEND_PLATFORMS.includes(project.platform as string)) {
return (
{currentNode}
);
}
if (FRONTEND_PLATFORMS.includes(project.platform as string)) {
return (
{currentNode}
);
}
}
return currentNode;
}}
);
} else {
nodes.push(currentNode);
}
if (children.length) {
nodes.push();
nodes.push(
);
}
if (descendants?.length) {
nodes.push();
nodes.push(
);
}
return {nodes};
}
function handleNode(key: string, organization: OrganizationSummary) {
trackAnalyticsEvent({
eventKey: 'quick_trace.node.clicked',
eventName: 'Quick Trace: Node clicked',
organization_id: parseInt(organization.id, 10),
node_key: key,
});
}
function handleDropdownItem(
key: string,
organization: OrganizationSummary,
extra: boolean
) {
trackAnalyticsEvent({
eventKey: 'quick_trace.dropdown.clicked' + (extra ? '_extra' : ''),
eventName: 'Quick Trace: Dropdown clicked',
organization_id: parseInt(organization.id, 10),
node_key: key,
});
}
type EventNodeSelectorProps = {
anchor: 'left' | 'right';
currentEvent: Event;
errorDest: ErrorDestination;
events: QuickTraceEvent[];
location: Location;
nodeKey: keyof typeof TOOLTIP_PREFIX;
organization: OrganizationSummary;
text: React.ReactNode;
transactionDest: TransactionDestination;
numEvents?: number;
};
function EventNodeSelector({
location,
organization,
events = [],
text,
currentEvent,
nodeKey,
anchor,
errorDest,
transactionDest,
numEvents = 5,
}: EventNodeSelectorProps) {
let errors: TraceError[] = events.flatMap(event => event.errors ?? []);
let type: keyof Theme['tag'] = nodeKey === 'current' ? 'black' : 'white';
const hasErrors = errors.length > 0;
if (hasErrors) {
type = nodeKey === 'current' ? 'error' : 'warning';
text = (
{text}
);
}
// make sure to exclude the current event from the dropdown
events = events.filter(event => event.event_id !== currentEvent.id);
errors = errors.filter(error => error.event_id !== currentEvent.id);
if (events.length + errors.length === 0) {
return {text};
}
if (events.length + errors.length === 1) {
/**
* When there is only 1 event, clicking the node should take the user directly to
* the event without additional steps.
*/
const hoverText = errors.length ? (
t('View the error for this Transaction')
) : (
);
const target = errors.length
? generateSingleErrorTarget(errors[0], organization, location, errorDest)
: generateSingleTransactionTarget(
events[0],
organization,
location,
transactionDest
);
return (
handleNode(nodeKey, organization)}
type={type}
shouldOffset={hasErrors}
/>
);
}
/**
* When there is more than 1 event, clicking the node should expand a dropdown to
* allow the user to select which event to go to.
*/
const hoverText = tct('View [eventPrefix] [eventType]', {
eventPrefix: TOOLTIP_PREFIX[nodeKey],
eventType:
errors.length && events.length
? 'events'
: events.length
? 'transactions'
: 'errors',
});
return (
}
anchorRight={anchor === 'right'}
>
{errors.length > 0 && (
{tn('Related Error', 'Related Errors', errors.length)}
)}
{errors.slice(0, numEvents).map(error => {
const target = generateSingleErrorTarget(
error,
organization,
location,
errorDest
);
return (
handleDropdownItem(nodeKey, organization, false)}
organization={organization}
anchor={anchor}
/>
);
})}
{events.length > 0 && (
{tn('Transaction', 'Transactions', events.length)}
)}
{events.slice(0, numEvents).map(event => {
const target = generateSingleTransactionTarget(
event,
organization,
location,
transactionDest
);
return (
handleDropdownItem(nodeKey, organization, false)}
allowDefaultEvent
organization={organization}
subtext={getDuration(
event['transaction.duration'] / 1000,
event['transaction.duration'] < 1000 ? 0 : 2,
true
)}
anchor={anchor}
/>
);
})}
{(errors.length > numEvents || events.length > numEvents) && (
handleDropdownItem(nodeKey, organization, true)}
>
{t('View all events')}
)}
);
}
type DropdownNodeProps = {
anchor: 'left' | 'right';
event: TraceError | QuickTraceEvent;
organization: OrganizationSummary;
allowDefaultEvent?: boolean;
onSelect?: (eventKey: any) => void;
subtext?: string;
to?: LocationDescriptor;
};
function DropdownNodeItem({
event,
onSelect,
to,
allowDefaultEvent,
organization,
subtext,
anchor,
}: DropdownNodeProps) {
return (
{({projects}) => {
const project = projects.find(p => p.slug === event.project_slug);
return (
);
}}
{isQuickTraceEvent(event) ? (
) : (
)}
{subtext && {subtext}}
);
}
type EventNodeProps = {
hoverText: React.ReactNode;
text: React.ReactNode;
onClick?: (eventKey: any) => void;
shouldOffset?: boolean;
to?: LocationDescriptor;
type?: keyof Theme['tag'];
};
function StyledEventNode({
text,
hoverText,
to,
onClick,
type = 'white',
shouldOffset = false,
}: EventNodeProps) {
return (
{text}
);
}
type MissingServiceProps = Pick & {
connectorSide: 'left' | 'right';
platform: string;
};
type MissingServiceState = {
hideMissing: boolean;
};
const HIDE_MISSING_SERVICE_KEY = 'quick-trace:hide-missing-services';
// 30 days
const HIDE_MISSING_EXPIRES = 1000 * 60 * 60 * 24 * 30;
function readHideMissingServiceState() {
const value = localStorage.getItem(HIDE_MISSING_SERVICE_KEY);
if (value === null) {
return false;
}
const expires = parseInt(value, 10);
const now = new Date().getTime();
return expires > now;
}
class MissingServiceNode extends Component {
state: MissingServiceState = {
hideMissing: readHideMissingServiceState(),
};
dismissMissingService = () => {
const {organization, platform} = this.props;
const now = new Date().getTime();
localStorage.setItem(
HIDE_MISSING_SERVICE_KEY,
(now + HIDE_MISSING_EXPIRES).toString()
);
this.setState({hideMissing: true});
trackAnalyticsEvent({
eventKey: 'quick_trace.missing_service.dismiss',
eventName: 'Quick Trace: Missing Service Dismissed',
organization_id: parseInt(organization.id, 10),
platform,
});
};
trackExternalLink = () => {
const {organization, platform} = this.props;
trackAnalyticsEvent({
eventKey: 'quick_trace.missing_service.docs',
eventName: 'Quick Trace: Missing Service Clicked',
organization_id: parseInt(organization.id, 10),
platform,
});
};
render() {
const {hideMissing} = this.state;
const {anchor, connectorSide, platform} = this.props;
if (hideMissing) {
return null;
}
const docPlatform = getDocsPlatform(platform, true);
const docsHref =
docPlatform === null || docPlatform === 'javascript'
? 'https://docs.sentry.io/platforms/javascript/performance/connect-services/'
: `https://docs.sentry.io/platforms/${docPlatform}/performance/connecting-services`;
return (
{connectorSide === 'left' && }
}
anchorRight={anchor === 'right'}
>
{t('Connect to a service')}
{t('Dismiss')}
{connectorSide === 'right' && }
);
}
}