transaction.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import {createRef, Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import omit from 'lodash/omit';
  5. import {Button} from 'sentry/components/button';
  6. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  7. import DateTime from 'sentry/components/dateTime';
  8. import {Chunk} from 'sentry/components/events/contexts/chunk';
  9. import {EventAttachments} from 'sentry/components/events/eventAttachments';
  10. import {
  11. isNotMarkMeasurement,
  12. isNotPerformanceScoreMeasurement,
  13. TraceEventCustomPerformanceMetric,
  14. } from 'sentry/components/events/eventCustomPerformanceMetrics';
  15. import {Entries} from 'sentry/components/events/eventEntries';
  16. import {EventEvidence} from 'sentry/components/events/eventEvidence';
  17. import {EventExtraData} from 'sentry/components/events/eventExtraData';
  18. import {EventSdk} from 'sentry/components/events/eventSdk';
  19. import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
  20. import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs';
  21. import {getFormattedTimeRangeWithLeadingAndTrailingZero} from 'sentry/components/events/interfaces/spans/utils';
  22. import {generateStats} from 'sentry/components/events/opsBreakdown';
  23. import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration';
  24. import FileSize from 'sentry/components/fileSize';
  25. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  26. import Link from 'sentry/components/links/link';
  27. import LoadingIndicator from 'sentry/components/loadingIndicator';
  28. import PerformanceDuration from 'sentry/components/performanceDuration';
  29. import QuestionTooltip from 'sentry/components/questionTooltip';
  30. import {Tooltip} from 'sentry/components/tooltip';
  31. import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
  32. import {IconChevron, IconOpen} from 'sentry/icons';
  33. import {t} from 'sentry/locale';
  34. import {space} from 'sentry/styles/space';
  35. import {
  36. type EntryBreadcrumbs,
  37. EntryType,
  38. type EventTransaction,
  39. type Organization,
  40. } from 'sentry/types';
  41. import {objectIsEmpty} from 'sentry/utils';
  42. import {trackAnalytics} from 'sentry/utils/analytics';
  43. import getDynamicText from 'sentry/utils/getDynamicText';
  44. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  45. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  46. import {useApiQuery} from 'sentry/utils/queryClient';
  47. import useProjects from 'sentry/utils/useProjects';
  48. import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
  49. import {CustomMetricsEventData} from 'sentry/views/ddm/customMetricsEventData';
  50. import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/virtualizedViewManager';
  51. import {Row, Tags} from 'sentry/views/performance/traceDetails/styles';
  52. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  53. import type {TraceTree, TraceTreeNode} from '../../traceTree';
  54. import IssueList from './issues/issueList';
  55. import {TraceDrawerComponents} from './styles';
  56. function OpsBreakdown({event}: {event: EventTransaction}) {
  57. const [showingAll, setShowingAll] = useState(false);
  58. const breakdown = event && generateStats(event, {type: 'no_filter'});
  59. if (!breakdown) {
  60. return null;
  61. }
  62. const renderText = showingAll ? t('Show less') : t('Show more') + '...';
  63. return (
  64. breakdown && (
  65. <Row
  66. title={
  67. <TraceDrawerComponents.FlexBox style={{gap: '5px'}}>
  68. {t('Ops Breakdown')}
  69. <QuestionTooltip
  70. title={t('Applicable to the children of this event only')}
  71. size="xs"
  72. />
  73. </TraceDrawerComponents.FlexBox>
  74. }
  75. >
  76. <div style={{display: 'flex', flexDirection: 'column', gap: space(0.25)}}>
  77. {breakdown.slice(0, showingAll ? breakdown.length : 5).map(currOp => {
  78. const {name, percentage, totalInterval} = currOp;
  79. const operationName = typeof name === 'string' ? name : t('Other');
  80. const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞';
  81. return (
  82. <div key={operationName}>
  83. {operationName}:{' '}
  84. <PerformanceDuration seconds={totalInterval} abbreviation /> ({pctLabel}%)
  85. </div>
  86. );
  87. })}
  88. {breakdown.length > 5 && (
  89. <a onClick={() => setShowingAll(prev => !prev)}>{renderText}</a>
  90. )}
  91. </div>
  92. </Row>
  93. )
  94. );
  95. }
  96. function BreadCrumbsSection({
  97. event,
  98. organization,
  99. }: {
  100. event: EventTransaction;
  101. organization: Organization;
  102. }) {
  103. const [showBreadCrumbs, setShowBreadCrumbs] = useState(false);
  104. const breadCrumbsContainerRef = createRef<HTMLDivElement>();
  105. useEffect(() => {
  106. setTimeout(() => {
  107. if (showBreadCrumbs) {
  108. breadCrumbsContainerRef.current?.scrollIntoView({
  109. behavior: 'smooth',
  110. block: 'end',
  111. });
  112. }
  113. }, 100);
  114. }, [showBreadCrumbs, breadCrumbsContainerRef]);
  115. const matchingEntry: EntryBreadcrumbs | undefined = event?.entries.find(
  116. (entry): entry is EntryBreadcrumbs => entry.type === EntryType.BREADCRUMBS
  117. );
  118. if (!matchingEntry) {
  119. return null;
  120. }
  121. const renderText = showBreadCrumbs ? t('Hide Breadcrumbs') : t('Show Breadcrumbs');
  122. const chevron = <IconChevron size="xs" direction={showBreadCrumbs ? 'up' : 'down'} />;
  123. return (
  124. <Fragment>
  125. <a
  126. style={{display: 'flex', alignItems: 'center', gap: space(0.5)}}
  127. onClick={() => {
  128. setShowBreadCrumbs(prev => !prev);
  129. }}
  130. >
  131. {renderText} {chevron}
  132. </a>
  133. <div ref={breadCrumbsContainerRef}>
  134. {showBreadCrumbs && (
  135. <Breadcrumbs
  136. hideTitle
  137. data={matchingEntry.data}
  138. event={event}
  139. organization={organization}
  140. />
  141. )}
  142. </div>
  143. </Fragment>
  144. );
  145. }
  146. type TransactionDetailProps = {
  147. location: Location;
  148. manager: VirtualizedViewManager;
  149. node: TraceTreeNode<TraceTree.Transaction>;
  150. organization: Organization;
  151. scrollToNode: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
  152. };
  153. export function TransactionNodeDetails({
  154. node,
  155. organization,
  156. location,
  157. scrollToNode,
  158. }: TransactionDetailProps) {
  159. const {projects} = useProjects();
  160. const {data: event} = useApiQuery<EventTransaction>(
  161. [
  162. `/organizations/${organization.slug}/events/${node.value.project_slug}:${node.value.event_id}/`,
  163. {
  164. query: {
  165. referrer: 'trace-details-summary',
  166. },
  167. },
  168. ],
  169. {
  170. staleTime: 0,
  171. enabled: !!node,
  172. }
  173. );
  174. const relatedIssues = useMemo(() => {
  175. return [...node.value.errors, ...node.value.performance_issues];
  176. }, [node.value.errors, node.value.performance_issues]);
  177. if (!event) {
  178. return <LoadingIndicator />;
  179. }
  180. const {user, contexts, projectSlug} = event;
  181. const {feedback} = contexts ?? {};
  182. const eventJsonUrl = `/api/0/projects/${organization.slug}/${node.value.project_slug}/events/${node.value.event_id}/json/`;
  183. const project = projects.find(proj => proj.slug === event?.projectSlug);
  184. const {errors, performance_issues} = node.value;
  185. const hasIssues = errors.length + performance_issues.length > 0;
  186. const startTimestamp = Math.min(node.value.start_timestamp, node.value.timestamp);
  187. const endTimestamp = Math.max(node.value.start_timestamp, node.value.timestamp);
  188. const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
  189. getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
  190. const duration = (endTimestamp - startTimestamp) * 1000;
  191. const durationString = `${Number(duration.toFixed(3)).toLocaleString()}ms`;
  192. const measurementNames = Object.keys(node.value.measurements ?? {})
  193. .filter(name => isCustomMeasurement(`measurements.${name}`))
  194. .filter(isNotMarkMeasurement)
  195. .filter(isNotPerformanceScoreMeasurement)
  196. .sort();
  197. const renderMeasurements = () => {
  198. if (!event) {
  199. return null;
  200. }
  201. const {measurements} = event;
  202. const measurementKeys = Object.keys(measurements ?? {})
  203. .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
  204. .sort();
  205. if (!measurements || measurementKeys.length <= 0) {
  206. return null;
  207. }
  208. return (
  209. <Fragment>
  210. {measurementKeys.map(measurement => (
  211. <Row
  212. key={measurement}
  213. title={WEB_VITAL_DETAILS[`measurements.${measurement}`]?.name}
  214. >
  215. <PerformanceDuration
  216. milliseconds={Number(measurements[measurement].value.toFixed(3))}
  217. abbreviation
  218. />
  219. </Row>
  220. ))}
  221. </Fragment>
  222. );
  223. };
  224. const renderGoToProfileButton = () => {
  225. if (!node.value.profile_id) {
  226. return null;
  227. }
  228. const target = generateProfileFlamechartRoute({
  229. orgSlug: organization.slug,
  230. projectSlug: node.value.project_slug,
  231. profileId: node.value.profile_id,
  232. });
  233. function handleOnClick() {
  234. trackAnalytics('profiling_views.go_to_flamegraph', {
  235. organization,
  236. source: 'performance.trace_view',
  237. });
  238. }
  239. return (
  240. <TraceDrawerComponents.Button size="xs" to={target} onClick={handleOnClick}>
  241. {t('View Profile')}
  242. </TraceDrawerComponents.Button>
  243. );
  244. };
  245. return (
  246. <TraceDrawerComponents.DetailContainer>
  247. <TraceDrawerComponents.HeaderContainer>
  248. <TraceDrawerComponents.Title>
  249. <Tooltip title={node.value.project_slug}>
  250. <ProjectBadge
  251. project={project ? project : {slug: node.value.project_slug}}
  252. avatarSize={30}
  253. hideName
  254. />
  255. </Tooltip>
  256. <div>
  257. <div>{t('transaction')}</div>
  258. <TraceDrawerComponents.TitleOp>
  259. {' '}
  260. {node.value['transaction.op']}
  261. </TraceDrawerComponents.TitleOp>
  262. </div>
  263. </TraceDrawerComponents.Title>
  264. <TraceDrawerComponents.Actions>
  265. <Button size="xs" onClick={_e => scrollToNode(node)}>
  266. {t('Show in view')}
  267. </Button>
  268. <Button
  269. size="xs"
  270. icon={<IconOpen />}
  271. href={eventJsonUrl}
  272. external
  273. onClick={() =>
  274. trackAnalytics('performance_views.event_details.json_button_click', {
  275. organization,
  276. })
  277. }
  278. >
  279. {t('JSON')} (<FileSize bytes={event?.size} />)
  280. </Button>
  281. </TraceDrawerComponents.Actions>
  282. </TraceDrawerComponents.HeaderContainer>
  283. {hasIssues && <IssueList organization={organization} issues={relatedIssues} />}
  284. <TraceDrawerComponents.Table className="table key-value">
  285. <tbody>
  286. <Row title={t('Event ID')}>
  287. {node.value.event_id}
  288. <CopyToClipboardButton
  289. borderless
  290. size="zero"
  291. iconSize="xs"
  292. text={`${window.location.href.replace(window.location.hash, '')}#txn-${
  293. node.value.event_id
  294. }`}
  295. />
  296. </Row>
  297. <Row title={t('Description')}>
  298. <Link
  299. to={transactionSummaryRouteWithQuery({
  300. orgSlug: organization.slug,
  301. transaction: node.value.transaction,
  302. query: omit(location.query, Object.values(PAGE_URL_PARAM)),
  303. projectID: String(node.value.project_id),
  304. })}
  305. >
  306. {node.value.transaction}
  307. </Link>
  308. </Row>
  309. {node.value.profile_id && (
  310. <Row title="Profile ID" extra={renderGoToProfileButton()}>
  311. {node.value.profile_id}
  312. </Row>
  313. )}
  314. <Row title="Duration">{durationString}</Row>
  315. <Row title="Date Range">
  316. {getDynamicText({
  317. fixed: 'Mar 19, 2021 11:06:27 AM UTC',
  318. value: (
  319. <Fragment>
  320. <DateTime date={startTimestamp * 1000} />
  321. {` (${startTimeWithLeadingZero})`}
  322. </Fragment>
  323. ),
  324. })}
  325. <br />
  326. {getDynamicText({
  327. fixed: 'Mar 19, 2021 11:06:28 AM UTC',
  328. value: (
  329. <Fragment>
  330. <DateTime date={endTimestamp * 1000} />
  331. {` (${endTimeWithLeadingZero})`}
  332. </Fragment>
  333. ),
  334. })}
  335. </Row>
  336. <OpsBreakdown event={event} />
  337. {renderMeasurements()}
  338. <Tags
  339. enableHiding
  340. location={location}
  341. organization={organization}
  342. tags={event.tags}
  343. event={node.value}
  344. />
  345. {measurementNames.length > 0 && (
  346. <tr>
  347. <td className="key">{t('Measurements')}</td>
  348. <td className="value">
  349. <Measurements>
  350. {measurementNames.map(name => {
  351. return (
  352. event && (
  353. <TraceEventCustomPerformanceMetric
  354. key={name}
  355. event={event}
  356. name={name}
  357. location={location}
  358. organization={organization}
  359. source={undefined}
  360. isHomepage={false}
  361. />
  362. )
  363. );
  364. })}
  365. </Measurements>
  366. </td>
  367. </tr>
  368. )}
  369. </tbody>
  370. </TraceDrawerComponents.Table>
  371. {project && <EventEvidence event={event} project={project} />}
  372. {projectSlug && (
  373. <Entries
  374. definedEvent={event}
  375. projectSlug={projectSlug}
  376. group={undefined}
  377. organization={organization}
  378. isShare={false}
  379. hideBeforeReplayEntries
  380. hideBreadCrumbs
  381. />
  382. )}
  383. {!objectIsEmpty(feedback) && (
  384. <Chunk
  385. key="feedback"
  386. type="feedback"
  387. alias="feedback"
  388. group={undefined}
  389. event={event}
  390. value={feedback}
  391. />
  392. )}
  393. {user && !objectIsEmpty(user) && (
  394. <Chunk
  395. key="user"
  396. type="user"
  397. alias="user"
  398. group={undefined}
  399. event={event}
  400. value={user}
  401. />
  402. )}
  403. <EventExtraData event={event} />
  404. <EventSdk sdk={event.sdk} meta={event._meta?.sdk} />
  405. {event._metrics_summary ? (
  406. <CustomMetricsEventData
  407. metricsSummary={event._metrics_summary}
  408. startTimestamp={event.startTimestamp}
  409. />
  410. ) : null}
  411. <BreadCrumbsSection event={event} organization={organization} />
  412. {projectSlug && <EventAttachments event={event} projectSlug={projectSlug} />}
  413. {project && <EventViewHierarchy event={event} project={project} />}
  414. {projectSlug && (
  415. <EventRRWebIntegration
  416. event={event}
  417. orgId={organization.slug}
  418. projectSlug={projectSlug}
  419. />
  420. )}
  421. </TraceDrawerComponents.DetailContainer>
  422. );
  423. }
  424. const Measurements = styled('div')`
  425. display: flex;
  426. flex-wrap: wrap;
  427. gap: ${space(1)};
  428. padding-top: 10px;
  429. `;