transaction.tsx 16 KB

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