index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. import {Component, Fragment} from 'react';
  2. import {Location, LocationDescriptor} from 'history';
  3. import DropdownLink from 'sentry/components/dropdownLink';
  4. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  5. import {
  6. ErrorDestination,
  7. generateSingleErrorTarget,
  8. generateSingleTransactionTarget,
  9. generateTraceTarget,
  10. isQuickTraceEvent,
  11. TransactionDestination,
  12. } from 'sentry/components/quickTrace/utils';
  13. import Tooltip from 'sentry/components/tooltip';
  14. import {backend, frontend, mobile, serverless} from 'sentry/data/platformCategories';
  15. import {IconFire} from 'sentry/icons';
  16. import {t, tct, tn} from 'sentry/locale';
  17. import {OrganizationSummary} from 'sentry/types';
  18. import {Event} from 'sentry/types/event';
  19. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  20. import {getDocsPlatform} from 'sentry/utils/docs';
  21. import {getDuration} from 'sentry/utils/formatters';
  22. import localStorage from 'sentry/utils/localStorage';
  23. import {
  24. QuickTrace as QuickTraceType,
  25. QuickTraceEvent,
  26. TraceError,
  27. } from 'sentry/utils/performance/quickTrace/types';
  28. import {parseQuickTrace} from 'sentry/utils/performance/quickTrace/utils';
  29. import Projects from 'sentry/utils/projects';
  30. import {Theme} from 'sentry/utils/theme';
  31. const FRONTEND_PLATFORMS: string[] = [...frontend, ...mobile];
  32. const BACKEND_PLATFORMS: string[] = [...backend, ...serverless];
  33. import {
  34. DropdownContainer,
  35. DropdownItem,
  36. DropdownItemSubContainer,
  37. DropdownMenuHeader,
  38. ErrorNodeContent,
  39. EventNode,
  40. ExternalDropdownLink,
  41. QuickTraceContainer,
  42. QuickTraceValue,
  43. SectionSubtext,
  44. SingleEventHoverText,
  45. TraceConnector,
  46. } from './styles';
  47. const TOOLTIP_PREFIX = {
  48. root: 'root',
  49. ancestors: 'ancestor',
  50. parent: 'parent',
  51. current: '',
  52. children: 'child',
  53. descendants: 'descendant',
  54. };
  55. type QuickTraceProps = Pick<
  56. EventNodeSelectorProps,
  57. 'anchor' | 'errorDest' | 'transactionDest'
  58. > & {
  59. event: Event;
  60. location: Location;
  61. organization: OrganizationSummary;
  62. quickTrace: QuickTraceType;
  63. };
  64. export default function QuickTrace({
  65. event,
  66. quickTrace,
  67. location,
  68. organization,
  69. anchor,
  70. errorDest,
  71. transactionDest,
  72. }: QuickTraceProps) {
  73. let parsedQuickTrace;
  74. try {
  75. parsedQuickTrace = parseQuickTrace(quickTrace, event, organization);
  76. } catch (error) {
  77. return <Fragment>{'\u2014'}</Fragment>;
  78. }
  79. const traceLength = quickTrace.trace && quickTrace.trace.length;
  80. const {root, ancestors, parent, children, descendants, current} = parsedQuickTrace;
  81. const nodes: React.ReactNode[] = [];
  82. if (root) {
  83. nodes.push(
  84. <EventNodeSelector
  85. key="root-node"
  86. location={location}
  87. organization={organization}
  88. events={[root]}
  89. currentEvent={event}
  90. text={t('Root')}
  91. anchor={anchor}
  92. nodeKey="root"
  93. errorDest={errorDest}
  94. transactionDest={transactionDest}
  95. />
  96. );
  97. nodes.push(<TraceConnector key="root-connector" />);
  98. }
  99. if (ancestors?.length) {
  100. nodes.push(
  101. <EventNodeSelector
  102. key="ancestors-node"
  103. location={location}
  104. organization={organization}
  105. events={ancestors}
  106. currentEvent={event}
  107. text={tn('%s Ancestor', '%s Ancestors', ancestors.length)}
  108. anchor={anchor}
  109. nodeKey="ancestors"
  110. errorDest={errorDest}
  111. transactionDest={transactionDest}
  112. />
  113. );
  114. nodes.push(<TraceConnector key="ancestors-connector" />);
  115. }
  116. if (parent) {
  117. nodes.push(
  118. <EventNodeSelector
  119. key="parent-node"
  120. location={location}
  121. organization={organization}
  122. events={[parent]}
  123. currentEvent={event}
  124. text={t('Parent')}
  125. anchor={anchor}
  126. nodeKey="parent"
  127. errorDest={errorDest}
  128. transactionDest={transactionDest}
  129. />
  130. );
  131. nodes.push(<TraceConnector key="parent-connector" />);
  132. }
  133. const currentNode = (
  134. <EventNodeSelector
  135. key="current-node"
  136. location={location}
  137. organization={organization}
  138. text={t('This Event')}
  139. events={[current]}
  140. currentEvent={event}
  141. anchor={anchor}
  142. nodeKey="current"
  143. errorDest={errorDest}
  144. transactionDest={transactionDest}
  145. />
  146. );
  147. if (traceLength === 1) {
  148. nodes.push(
  149. <Projects
  150. key="missing-services"
  151. orgId={organization.slug}
  152. slugs={[current.project_slug]}
  153. >
  154. {({projects}) => {
  155. const project = projects.find(p => p.slug === current.project_slug);
  156. if (project?.platform) {
  157. if (BACKEND_PLATFORMS.includes(project.platform as string)) {
  158. return (
  159. <Fragment>
  160. <MissingServiceNode
  161. anchor={anchor}
  162. organization={organization}
  163. platform={project.platform}
  164. connectorSide="right"
  165. />
  166. {currentNode}
  167. </Fragment>
  168. );
  169. }
  170. if (FRONTEND_PLATFORMS.includes(project.platform as string)) {
  171. return (
  172. <Fragment>
  173. {currentNode}
  174. <MissingServiceNode
  175. anchor={anchor}
  176. organization={organization}
  177. platform={project.platform}
  178. connectorSide="left"
  179. />
  180. </Fragment>
  181. );
  182. }
  183. }
  184. return currentNode;
  185. }}
  186. </Projects>
  187. );
  188. } else {
  189. nodes.push(currentNode);
  190. }
  191. if (children.length) {
  192. nodes.push(<TraceConnector key="children-connector" />);
  193. nodes.push(
  194. <EventNodeSelector
  195. key="children-node"
  196. location={location}
  197. organization={organization}
  198. events={children}
  199. currentEvent={event}
  200. text={tn('%s Child', '%s Children', children.length)}
  201. anchor={anchor}
  202. nodeKey="children"
  203. errorDest={errorDest}
  204. transactionDest={transactionDest}
  205. />
  206. );
  207. }
  208. if (descendants?.length) {
  209. nodes.push(<TraceConnector key="descendants-connector" />);
  210. nodes.push(
  211. <EventNodeSelector
  212. key="descendants-node"
  213. location={location}
  214. organization={organization}
  215. events={descendants}
  216. currentEvent={event}
  217. text={tn('%s Descendant', '%s Descendants', descendants.length)}
  218. anchor={anchor}
  219. nodeKey="descendants"
  220. errorDest={errorDest}
  221. transactionDest={transactionDest}
  222. />
  223. );
  224. }
  225. return <QuickTraceContainer>{nodes}</QuickTraceContainer>;
  226. }
  227. function handleNode(key: string, organization: OrganizationSummary) {
  228. trackAdvancedAnalyticsEvent('quick_trace.node.clicked', {
  229. organization: organization.id,
  230. node_key: key,
  231. });
  232. }
  233. function handleDropdownItem(
  234. key: string,
  235. organization: OrganizationSummary,
  236. extra: boolean
  237. ) {
  238. const eventKey = extra
  239. ? 'quick_trace.dropdown.clicked_extra'
  240. : 'quick_trace.dropdown.clicked';
  241. trackAdvancedAnalyticsEvent(eventKey, {
  242. organization: organization.id,
  243. node_key: key,
  244. });
  245. }
  246. type EventNodeSelectorProps = {
  247. anchor: 'left' | 'right';
  248. currentEvent: Event;
  249. errorDest: ErrorDestination;
  250. events: QuickTraceEvent[];
  251. location: Location;
  252. nodeKey: keyof typeof TOOLTIP_PREFIX;
  253. organization: OrganizationSummary;
  254. text: React.ReactNode;
  255. transactionDest: TransactionDestination;
  256. numEvents?: number;
  257. };
  258. function EventNodeSelector({
  259. location,
  260. organization,
  261. events = [],
  262. text,
  263. currentEvent,
  264. nodeKey,
  265. anchor,
  266. errorDest,
  267. transactionDest,
  268. numEvents = 5,
  269. }: EventNodeSelectorProps) {
  270. let errors: TraceError[] = events.flatMap(event => event.errors ?? []);
  271. let type: keyof Theme['tag'] = nodeKey === 'current' ? 'black' : 'white';
  272. const hasErrors = errors.length > 0;
  273. if (hasErrors) {
  274. type = nodeKey === 'current' ? 'error' : 'warning';
  275. text = (
  276. <ErrorNodeContent>
  277. <IconFire size="xs" />
  278. {text}
  279. </ErrorNodeContent>
  280. );
  281. }
  282. // make sure to exclude the current event from the dropdown
  283. events = events.filter(event => event.event_id !== currentEvent.id);
  284. errors = errors.filter(error => error.event_id !== currentEvent.id);
  285. if (events.length + errors.length === 0) {
  286. return <EventNode type={type}>{text}</EventNode>;
  287. }
  288. if (events.length + errors.length === 1) {
  289. /**
  290. * When there is only 1 event, clicking the node should take the user directly to
  291. * the event without additional steps.
  292. */
  293. const hoverText = errors.length ? (
  294. t('View the error for this Transaction')
  295. ) : (
  296. <SingleEventHoverText event={events[0]} />
  297. );
  298. const target = errors.length
  299. ? generateSingleErrorTarget(errors[0], organization, location, errorDest)
  300. : generateSingleTransactionTarget(
  301. events[0],
  302. organization,
  303. location,
  304. transactionDest
  305. );
  306. return (
  307. <StyledEventNode
  308. text={text}
  309. hoverText={hoverText}
  310. to={target}
  311. onClick={() => handleNode(nodeKey, organization)}
  312. type={type}
  313. />
  314. );
  315. }
  316. /**
  317. * When there is more than 1 event, clicking the node should expand a dropdown to
  318. * allow the user to select which event to go to.
  319. */
  320. const hoverText = tct('View [eventPrefix] [eventType]', {
  321. eventPrefix: TOOLTIP_PREFIX[nodeKey],
  322. eventType:
  323. errors.length && events.length
  324. ? 'events'
  325. : events.length
  326. ? 'transactions'
  327. : 'errors',
  328. });
  329. return (
  330. <DropdownContainer>
  331. <DropdownLink
  332. caret={false}
  333. title={<StyledEventNode text={text} hoverText={hoverText} type={type} />}
  334. anchorRight={anchor === 'right'}
  335. >
  336. {errors.length > 0 && (
  337. <DropdownMenuHeader first>
  338. {tn('Related Error', 'Related Errors', errors.length)}
  339. </DropdownMenuHeader>
  340. )}
  341. {errors.slice(0, numEvents).map(error => {
  342. const target = generateSingleErrorTarget(
  343. error,
  344. organization,
  345. location,
  346. errorDest
  347. );
  348. return (
  349. <DropdownNodeItem
  350. key={error.event_id}
  351. event={error}
  352. to={target}
  353. allowDefaultEvent
  354. onSelect={() => handleDropdownItem(nodeKey, organization, false)}
  355. organization={organization}
  356. anchor={anchor}
  357. />
  358. );
  359. })}
  360. {events.length > 0 && (
  361. <DropdownMenuHeader first={errors.length === 0}>
  362. {tn('Transaction', 'Transactions', events.length)}
  363. </DropdownMenuHeader>
  364. )}
  365. {events.slice(0, numEvents).map(event => {
  366. const target = generateSingleTransactionTarget(
  367. event,
  368. organization,
  369. location,
  370. transactionDest
  371. );
  372. return (
  373. <DropdownNodeItem
  374. key={event.event_id}
  375. event={event}
  376. to={target}
  377. onSelect={() => handleDropdownItem(nodeKey, organization, false)}
  378. allowDefaultEvent
  379. organization={organization}
  380. subtext={getDuration(
  381. event['transaction.duration'] / 1000,
  382. event['transaction.duration'] < 1000 ? 0 : 2,
  383. true
  384. )}
  385. anchor={anchor}
  386. />
  387. );
  388. })}
  389. {(errors.length > numEvents || events.length > numEvents) && (
  390. <DropdownItem
  391. to={generateTraceTarget(currentEvent, organization)}
  392. allowDefaultEvent
  393. onSelect={() => handleDropdownItem(nodeKey, organization, true)}
  394. >
  395. {t('View all events')}
  396. </DropdownItem>
  397. )}
  398. </DropdownLink>
  399. </DropdownContainer>
  400. );
  401. }
  402. type DropdownNodeProps = {
  403. anchor: 'left' | 'right';
  404. event: TraceError | QuickTraceEvent;
  405. organization: OrganizationSummary;
  406. allowDefaultEvent?: boolean;
  407. onSelect?: (eventKey: any) => void;
  408. subtext?: string;
  409. to?: LocationDescriptor;
  410. };
  411. function DropdownNodeItem({
  412. event,
  413. onSelect,
  414. to,
  415. allowDefaultEvent,
  416. organization,
  417. subtext,
  418. anchor,
  419. }: DropdownNodeProps) {
  420. return (
  421. <DropdownItem to={to} onSelect={onSelect} allowDefaultEvent={allowDefaultEvent}>
  422. <DropdownItemSubContainer>
  423. <Projects orgId={organization.slug} slugs={[event.project_slug]}>
  424. {({projects}) => {
  425. const project = projects.find(p => p.slug === event.project_slug);
  426. return (
  427. <ProjectBadge
  428. disableLink
  429. hideName
  430. project={project ? project : {slug: event.project_slug}}
  431. avatarSize={16}
  432. />
  433. );
  434. }}
  435. </Projects>
  436. {isQuickTraceEvent(event) ? (
  437. <QuickTraceValue
  438. value={event.transaction}
  439. // expand in the opposite direction of the anchor
  440. expandDirection={anchor === 'left' ? 'right' : 'left'}
  441. maxLength={35}
  442. leftTrim
  443. trimRegex={/\.|\//g}
  444. />
  445. ) : (
  446. <QuickTraceValue
  447. value={event.title}
  448. // expand in the opposite direction of the anchor
  449. expandDirection={anchor === 'left' ? 'right' : 'left'}
  450. maxLength={45}
  451. />
  452. )}
  453. </DropdownItemSubContainer>
  454. {subtext && <SectionSubtext>{subtext}</SectionSubtext>}
  455. </DropdownItem>
  456. );
  457. }
  458. type EventNodeProps = {
  459. hoverText: React.ReactNode;
  460. text: React.ReactNode;
  461. onClick?: (eventKey: any) => void;
  462. to?: LocationDescriptor;
  463. type?: keyof Theme['tag'];
  464. };
  465. function StyledEventNode({text, hoverText, to, onClick, type = 'white'}: EventNodeProps) {
  466. return (
  467. <Tooltip position="top" containerDisplayMode="inline-flex" title={hoverText}>
  468. <EventNode type={type} icon={null} to={to} onClick={onClick}>
  469. {text}
  470. </EventNode>
  471. </Tooltip>
  472. );
  473. }
  474. type MissingServiceProps = Pick<QuickTraceProps, 'anchor' | 'organization'> & {
  475. connectorSide: 'left' | 'right';
  476. platform: string;
  477. };
  478. type MissingServiceState = {
  479. hideMissing: boolean;
  480. };
  481. const HIDE_MISSING_SERVICE_KEY = 'quick-trace:hide-missing-services';
  482. // 30 days
  483. const HIDE_MISSING_EXPIRES = 1000 * 60 * 60 * 24 * 30;
  484. function readHideMissingServiceState() {
  485. const value = localStorage.getItem(HIDE_MISSING_SERVICE_KEY);
  486. if (value === null) {
  487. return false;
  488. }
  489. const expires = parseInt(value, 10);
  490. const now = new Date().getTime();
  491. return expires > now;
  492. }
  493. class MissingServiceNode extends Component<MissingServiceProps, MissingServiceState> {
  494. state: MissingServiceState = {
  495. hideMissing: readHideMissingServiceState(),
  496. };
  497. dismissMissingService = () => {
  498. const {organization, platform} = this.props;
  499. const now = new Date().getTime();
  500. localStorage.setItem(
  501. HIDE_MISSING_SERVICE_KEY,
  502. (now + HIDE_MISSING_EXPIRES).toString()
  503. );
  504. this.setState({hideMissing: true});
  505. trackAdvancedAnalyticsEvent('quick_trace.missing_service.dismiss', {
  506. organization: organization.id,
  507. platform,
  508. });
  509. };
  510. trackExternalLink = () => {
  511. const {organization, platform} = this.props;
  512. trackAdvancedAnalyticsEvent('quick_trace.missing_service.docs', {
  513. organization: organization.id,
  514. platform,
  515. });
  516. };
  517. render() {
  518. const {hideMissing} = this.state;
  519. const {anchor, connectorSide, platform} = this.props;
  520. if (hideMissing) {
  521. return null;
  522. }
  523. const docPlatform = getDocsPlatform(platform, true);
  524. const docsHref =
  525. docPlatform === null || docPlatform === 'javascript'
  526. ? 'https://docs.sentry.io/platforms/javascript/performance/connect-services/'
  527. : `https://docs.sentry.io/platforms/${docPlatform}/performance/connect-services`;
  528. return (
  529. <Fragment>
  530. {connectorSide === 'left' && <TraceConnector />}
  531. <DropdownContainer>
  532. <DropdownLink
  533. caret={false}
  534. title={
  535. <StyledEventNode
  536. type="white"
  537. hoverText={t('No services connected')}
  538. text="???"
  539. />
  540. }
  541. anchorRight={anchor === 'right'}
  542. >
  543. <DropdownItem width="small">
  544. <ExternalDropdownLink href={docsHref} onClick={this.trackExternalLink}>
  545. {t('Connect to a service')}
  546. </ExternalDropdownLink>
  547. </DropdownItem>
  548. <DropdownItem onSelect={this.dismissMissingService} width="small">
  549. {t('Dismiss')}
  550. </DropdownItem>
  551. </DropdownLink>
  552. </DropdownContainer>
  553. {connectorSide === 'right' && <TraceConnector />}
  554. </Fragment>
  555. );
  556. }
  557. }