index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. import * as React from 'react';
  2. import {Location, LocationDescriptor} from 'history';
  3. import DropdownLink from 'app/components/dropdownLink';
  4. import ProjectBadge from 'app/components/idBadge/projectBadge';
  5. import {
  6. ErrorDestination,
  7. generateSingleErrorTarget,
  8. generateSingleTransactionTarget,
  9. generateTraceTarget,
  10. isQuickTraceEvent,
  11. TransactionDestination,
  12. } from 'app/components/quickTrace/utils';
  13. import Tooltip from 'app/components/tooltip';
  14. import {backend, frontend, mobile, serverless} from 'app/data/platformCategories';
  15. import {IconFire} from 'app/icons';
  16. import {t, tct, tn} from 'app/locale';
  17. import {OrganizationSummary} from 'app/types';
  18. import {Event} from 'app/types/event';
  19. import {trackAnalyticsEvent} from 'app/utils/analytics';
  20. import {getDocsPlatform} from 'app/utils/docs';
  21. import {getDuration} from 'app/utils/formatters';
  22. import localStorage from 'app/utils/localStorage';
  23. import {
  24. QuickTrace as QuickTraceType,
  25. QuickTraceEvent,
  26. TraceError,
  27. } from 'app/utils/performance/quickTrace/types';
  28. import {parseQuickTrace} from 'app/utils/performance/quickTrace/utils';
  29. import Projects from 'app/utils/projects';
  30. import {Theme} from 'app/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. SectionSubtext,
  43. SingleEventHoverText,
  44. StyledTruncate,
  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. quickTrace: QuickTraceType;
  60. event: Event;
  61. location: Location;
  62. organization: OrganizationSummary;
  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 <React.Fragment>{'\u2014'}</React.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. <React.Fragment>
  160. <MissingServiceNode
  161. anchor={anchor}
  162. organization={organization}
  163. platform={project.platform}
  164. connectorSide="right"
  165. />
  166. {currentNode}
  167. </React.Fragment>
  168. );
  169. } else if (FRONTEND_PLATFORMS.includes(project.platform as string)) {
  170. return (
  171. <React.Fragment>
  172. {currentNode}
  173. <MissingServiceNode
  174. anchor={anchor}
  175. organization={organization}
  176. platform={project.platform}
  177. connectorSide="left"
  178. />
  179. </React.Fragment>
  180. );
  181. }
  182. }
  183. return currentNode;
  184. }}
  185. </Projects>
  186. );
  187. } else {
  188. nodes.push(currentNode);
  189. }
  190. if (children.length) {
  191. nodes.push(<TraceConnector key="children-connector" />);
  192. nodes.push(
  193. <EventNodeSelector
  194. key="children-node"
  195. location={location}
  196. organization={organization}
  197. events={children}
  198. currentEvent={event}
  199. text={tn('%s Child', '%s Children', children.length)}
  200. anchor={anchor}
  201. nodeKey="children"
  202. errorDest={errorDest}
  203. transactionDest={transactionDest}
  204. />
  205. );
  206. }
  207. if (descendants?.length) {
  208. nodes.push(<TraceConnector key="descendants-connector" />);
  209. nodes.push(
  210. <EventNodeSelector
  211. key="descendants-node"
  212. location={location}
  213. organization={organization}
  214. events={descendants}
  215. currentEvent={event}
  216. text={tn('%s Descendant', '%s Descendants', descendants.length)}
  217. anchor={anchor}
  218. nodeKey="descendants"
  219. errorDest={errorDest}
  220. transactionDest={transactionDest}
  221. />
  222. );
  223. }
  224. return <QuickTraceContainer>{nodes}</QuickTraceContainer>;
  225. }
  226. function handleNode(key: string, organization: OrganizationSummary) {
  227. trackAnalyticsEvent({
  228. eventKey: 'quick_trace.node.clicked',
  229. eventName: 'Quick Trace: Node clicked',
  230. organization_id: parseInt(organization.id, 10),
  231. node_key: key,
  232. });
  233. }
  234. function handleDropdownItem(
  235. key: string,
  236. organization: OrganizationSummary,
  237. extra: boolean
  238. ) {
  239. trackAnalyticsEvent({
  240. eventKey: 'quick_trace.dropdown.clicked' + (extra ? '_extra' : ''),
  241. eventName: 'Quick Trace: Dropdown clicked',
  242. organization_id: parseInt(organization.id, 10),
  243. node_key: key,
  244. });
  245. }
  246. type EventNodeSelectorProps = {
  247. location: Location;
  248. organization: OrganizationSummary;
  249. events: QuickTraceEvent[];
  250. text: React.ReactNode;
  251. currentEvent: Event;
  252. numEvents?: number;
  253. anchor: 'left' | 'right';
  254. nodeKey: keyof typeof TOOLTIP_PREFIX;
  255. errorDest: ErrorDestination;
  256. transactionDest: TransactionDestination;
  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. if (errors.length > 0) {
  273. type = nodeKey === 'current' ? 'error' : 'warning';
  274. text = (
  275. <ErrorNodeContent>
  276. <IconFire size="xs" />
  277. {text}
  278. </ErrorNodeContent>
  279. );
  280. }
  281. // make sure to exclude the current event from the dropdown
  282. events = events.filter(event => event.event_id !== currentEvent.id);
  283. errors = errors.filter(error => error.event_id !== currentEvent.id);
  284. if (events.length + errors.length === 0) {
  285. return <EventNode type={type}>{text}</EventNode>;
  286. } else if (events.length + errors.length === 1) {
  287. /**
  288. * When there is only 1 event, clicking the node should take the user directly to
  289. * the event without additional steps.
  290. */
  291. const hoverText = errors.length ? (
  292. t('View the error for this Transaction')
  293. ) : (
  294. <SingleEventHoverText event={events[0]} />
  295. );
  296. const target = errors.length
  297. ? generateSingleErrorTarget(errors[0], organization, location, errorDest)
  298. : generateSingleTransactionTarget(
  299. events[0],
  300. organization,
  301. location,
  302. transactionDest
  303. );
  304. return (
  305. <StyledEventNode
  306. text={text}
  307. hoverText={hoverText}
  308. to={target}
  309. onClick={() => handleNode(nodeKey, organization)}
  310. type={type}
  311. />
  312. );
  313. } else {
  314. /**
  315. * When there is more than 1 event, clicking the node should expand a dropdown to
  316. * allow the user to select which event to go to.
  317. */
  318. const hoverText = tct('View [eventPrefix] [eventType]', {
  319. eventPrefix: TOOLTIP_PREFIX[nodeKey],
  320. eventType:
  321. errors.length && events.length
  322. ? 'events'
  323. : events.length
  324. ? 'transactions'
  325. : 'errors',
  326. });
  327. return (
  328. <DropdownContainer>
  329. <DropdownLink
  330. caret={false}
  331. title={<StyledEventNode text={text} hoverText={hoverText} type={type} />}
  332. anchorRight={anchor === 'right'}
  333. >
  334. {errors.length > 0 && (
  335. <DropdownMenuHeader first>
  336. {tn('Related Error', 'Related Errors', errors.length)}
  337. </DropdownMenuHeader>
  338. )}
  339. {errors.slice(0, numEvents).map(error => {
  340. const target = generateSingleErrorTarget(
  341. error,
  342. organization,
  343. location,
  344. errorDest
  345. );
  346. return (
  347. <DropdownNodeItem
  348. key={error.event_id}
  349. event={error}
  350. to={target}
  351. allowDefaultEvent
  352. onSelect={() => handleDropdownItem(nodeKey, organization, false)}
  353. organization={organization}
  354. anchor={anchor}
  355. />
  356. );
  357. })}
  358. {events.length > 0 && (
  359. <DropdownMenuHeader first={errors.length === 0}>
  360. {tn('Transaction', 'Transactions', events.length)}
  361. </DropdownMenuHeader>
  362. )}
  363. {events.slice(0, numEvents).map(event => {
  364. const target = generateSingleTransactionTarget(
  365. event,
  366. organization,
  367. location,
  368. transactionDest
  369. );
  370. return (
  371. <DropdownNodeItem
  372. key={event.event_id}
  373. event={event}
  374. to={target}
  375. onSelect={() => handleDropdownItem(nodeKey, organization, false)}
  376. allowDefaultEvent
  377. organization={organization}
  378. subtext={getDuration(
  379. event['transaction.duration'] / 1000,
  380. event['transaction.duration'] < 1000 ? 0 : 2,
  381. true
  382. )}
  383. anchor={anchor}
  384. />
  385. );
  386. })}
  387. {(errors.length > numEvents || events.length > numEvents) && (
  388. <DropdownItem
  389. to={generateTraceTarget(currentEvent, organization)}
  390. allowDefaultEvent
  391. onSelect={() => handleDropdownItem(nodeKey, organization, true)}
  392. >
  393. {t('View all events')}
  394. </DropdownItem>
  395. )}
  396. </DropdownLink>
  397. </DropdownContainer>
  398. );
  399. }
  400. }
  401. type DropdownNodeProps = {
  402. event: TraceError | QuickTraceEvent;
  403. organization: OrganizationSummary;
  404. anchor: 'left' | 'right';
  405. allowDefaultEvent?: boolean;
  406. onSelect?: (eventKey: any) => void;
  407. to?: LocationDescriptor;
  408. subtext?: string;
  409. };
  410. function DropdownNodeItem({
  411. event,
  412. onSelect,
  413. to,
  414. allowDefaultEvent,
  415. organization,
  416. subtext,
  417. anchor,
  418. }: DropdownNodeProps) {
  419. return (
  420. <DropdownItem to={to} onSelect={onSelect} allowDefaultEvent={allowDefaultEvent}>
  421. <DropdownItemSubContainer>
  422. <Projects orgId={organization.slug} slugs={[event.project_slug]}>
  423. {({projects}) => {
  424. const project = projects.find(p => p.slug === event.project_slug);
  425. return (
  426. <ProjectBadge
  427. disableLink
  428. hideName
  429. project={project ? project : {slug: event.project_slug}}
  430. avatarSize={16}
  431. />
  432. );
  433. }}
  434. </Projects>
  435. {isQuickTraceEvent(event) ? (
  436. <StyledTruncate
  437. value={event.transaction}
  438. // expand in the opposite direction of the anchor
  439. expandDirection={anchor === 'left' ? 'right' : 'left'}
  440. maxLength={35}
  441. leftTrim
  442. trimRegex={/\.|\//g}
  443. />
  444. ) : (
  445. <StyledTruncate
  446. value={event.title}
  447. // expand in the opposite direction of the anchor
  448. expandDirection={anchor === 'left' ? 'right' : 'left'}
  449. maxLength={45}
  450. />
  451. )}
  452. </DropdownItemSubContainer>
  453. {subtext && <SectionSubtext>{subtext}</SectionSubtext>}
  454. </DropdownItem>
  455. );
  456. }
  457. type EventNodeProps = {
  458. text: React.ReactNode;
  459. hoverText: React.ReactNode;
  460. to?: LocationDescriptor;
  461. onClick?: (eventKey: any) => void;
  462. type?: keyof Theme['tag'];
  463. };
  464. function StyledEventNode({text, hoverText, to, onClick, type = 'white'}: EventNodeProps) {
  465. return (
  466. <Tooltip position="top" containerDisplayMode="inline-flex" title={hoverText}>
  467. <EventNode type={type} icon={null} to={to} onClick={onClick}>
  468. {text}
  469. </EventNode>
  470. </Tooltip>
  471. );
  472. }
  473. type MissingServiceProps = Pick<QuickTraceProps, 'anchor' | 'organization'> & {
  474. connectorSide: 'left' | 'right';
  475. platform: string;
  476. };
  477. type MissingServiceState = {
  478. hideMissing: boolean;
  479. };
  480. const HIDE_MISSING_SERVICE_KEY = 'quick-trace:hide-missing-services';
  481. // 30 days
  482. const HIDE_MISSING_EXPIRES = 1000 * 60 * 60 * 24 * 30;
  483. function readHideMissingServiceState() {
  484. const value = localStorage.getItem(HIDE_MISSING_SERVICE_KEY);
  485. if (value === null) {
  486. return false;
  487. }
  488. const expires = parseInt(value, 10);
  489. const now = new Date().getTime();
  490. return expires > now;
  491. }
  492. class MissingServiceNode extends React.Component<
  493. MissingServiceProps,
  494. MissingServiceState
  495. > {
  496. state: MissingServiceState = {
  497. hideMissing: readHideMissingServiceState(),
  498. };
  499. dismissMissingService = () => {
  500. const {organization, platform} = this.props;
  501. const now = new Date().getTime();
  502. localStorage.setItem(
  503. HIDE_MISSING_SERVICE_KEY,
  504. (now + HIDE_MISSING_EXPIRES).toString()
  505. );
  506. this.setState({hideMissing: true});
  507. trackAnalyticsEvent({
  508. eventKey: 'quick_trace.missing_service.dismiss',
  509. eventName: 'Quick Trace: Missing Service Dismissed',
  510. organization_id: parseInt(organization.id, 10),
  511. platform,
  512. });
  513. };
  514. trackExternalLink = () => {
  515. const {organization, platform} = this.props;
  516. trackAnalyticsEvent({
  517. eventKey: 'quick_trace.missing_service.docs',
  518. eventName: 'Quick Trace: Missing Service Clicked',
  519. organization_id: parseInt(organization.id, 10),
  520. platform,
  521. });
  522. };
  523. render() {
  524. const {hideMissing} = this.state;
  525. const {anchor, connectorSide, platform} = this.props;
  526. if (hideMissing) {
  527. return null;
  528. }
  529. const docPlatform = getDocsPlatform(platform, true);
  530. const docsHref =
  531. docPlatform === null || docPlatform === 'javascript'
  532. ? 'https://docs.sentry.io/platforms/javascript/performance/connect-services/'
  533. : `https://docs.sentry.io/platforms/${docPlatform}/performance#connecting-services`;
  534. return (
  535. <React.Fragment>
  536. {connectorSide === 'left' && <TraceConnector />}
  537. <DropdownContainer>
  538. <DropdownLink
  539. caret={false}
  540. title={
  541. <StyledEventNode
  542. type="white"
  543. hoverText={t('No services connected')}
  544. text="???"
  545. />
  546. }
  547. anchorRight={anchor === 'right'}
  548. >
  549. <DropdownItem width="small">
  550. <ExternalDropdownLink href={docsHref} onClick={this.trackExternalLink}>
  551. {t('Connect to a service')}
  552. </ExternalDropdownLink>
  553. </DropdownItem>
  554. <DropdownItem onSelect={this.dismissMissingService} width="small">
  555. {t('Dismiss')}
  556. </DropdownItem>
  557. </DropdownLink>
  558. </DropdownContainer>
  559. {connectorSide === 'right' && <TraceConnector />}
  560. </React.Fragment>
  561. );
  562. }
  563. }