content.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import omit from 'lodash/omit';
  5. import type {DropdownOption} from 'sentry/components/discover/transactionsList';
  6. import TransactionsList from 'sentry/components/discover/transactionsList';
  7. import SearchBar from 'sentry/components/events/searchBar';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  10. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  11. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  12. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  13. import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
  14. import {SuspectFunctionsTable} from 'sentry/components/profiling/suspectFunctions/suspectFunctionsTable';
  15. import type {ActionBarItem} from 'sentry/components/smartSearchBar';
  16. import {Tooltip} from 'sentry/components/tooltip';
  17. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  18. import {IconWarning} from 'sentry/icons';
  19. import {t} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  21. import type {Organization} from 'sentry/types/organization';
  22. import type {Project} from 'sentry/types/project';
  23. import {defined, generateQueryWithTag} from 'sentry/utils';
  24. import {trackAnalytics} from 'sentry/utils/analytics';
  25. import {browserHistory} from 'sentry/utils/browserHistory';
  26. import type EventView from 'sentry/utils/discover/eventView';
  27. import {
  28. formatTagKey,
  29. isRelativeSpanOperationBreakdownField,
  30. SPAN_OP_BREAKDOWN_FIELDS,
  31. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  32. } from 'sentry/utils/discover/fields';
  33. import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
  34. import type {MetricsEnhancedPerformanceDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  35. import {useMEPDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  36. import {decodeScalar} from 'sentry/utils/queryString';
  37. import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
  38. import {useRoutes} from 'sentry/utils/useRoutes';
  39. import withProjects from 'sentry/utils/withProjects';
  40. import type {Actions} from 'sentry/views/discover/table/cellAction';
  41. import {updateQuery} from 'sentry/views/discover/table/cellAction';
  42. import type {TableColumn} from 'sentry/views/discover/table/types';
  43. import Tags from 'sentry/views/discover/tags';
  44. import {canUseTransactionMetricsData} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
  45. import {
  46. PERCENTILE as VITAL_PERCENTILE,
  47. VITAL_GROUPS,
  48. } from 'sentry/views/performance/transactionSummary/transactionVitals/constants';
  49. import {isSummaryViewFrontend, isSummaryViewFrontendPageLoad} from '../../utils';
  50. import Filter, {
  51. decodeFilterFromLocation,
  52. filterToField,
  53. filterToSearchConditions,
  54. SpanOperationBreakdownFilter,
  55. } from '../filter';
  56. import {
  57. generateProfileLink,
  58. generateReplayLink,
  59. generateTraceLink,
  60. generateTransactionIdLink,
  61. normalizeSearchConditions,
  62. SidebarSpacer,
  63. TransactionFilterOptions,
  64. } from '../utils';
  65. import TransactionSummaryCharts from './charts';
  66. import {PerformanceAtScaleContextProvider} from './performanceAtScaleContext';
  67. import RelatedIssues from './relatedIssues';
  68. import SidebarCharts from './sidebarCharts';
  69. import StatusBreakdown from './statusBreakdown';
  70. import SuspectSpans from './suspectSpans';
  71. import {TagExplorer} from './tagExplorer';
  72. import UserStats from './userStats';
  73. type Props = {
  74. error: QueryError | null;
  75. eventView: EventView;
  76. isLoading: boolean;
  77. location: Location;
  78. onChangeFilter: (newFilter: SpanOperationBreakdownFilter) => void;
  79. organization: Organization;
  80. projectId: string;
  81. projects: Project[];
  82. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  83. totalValues: Record<string, number> | null;
  84. transactionName: string;
  85. };
  86. function SummaryContent({
  87. eventView,
  88. location,
  89. totalValues,
  90. spanOperationBreakdownFilter,
  91. organization,
  92. projects,
  93. isLoading,
  94. error,
  95. projectId,
  96. transactionName,
  97. onChangeFilter,
  98. }: Props) {
  99. const routes = useRoutes();
  100. const mepDataContext = useMEPDataContext();
  101. const handleSearch = useCallback(
  102. (query: string) => {
  103. const queryParams = normalizeDateTimeParams({
  104. ...(location.query || {}),
  105. query,
  106. });
  107. // do not propagate pagination when making a new search
  108. const searchQueryParams = omit(queryParams, 'cursor');
  109. browserHistory.push({
  110. pathname: location.pathname,
  111. query: searchQueryParams,
  112. });
  113. },
  114. [location]
  115. );
  116. function generateTagUrl(key: string, value: string) {
  117. const query = generateQueryWithTag(location.query, {key: formatTagKey(key), value});
  118. return {
  119. ...location,
  120. query,
  121. };
  122. }
  123. function handleCellAction(column: TableColumn<React.ReactText>) {
  124. return (action: Actions, value: React.ReactText) => {
  125. const searchConditions = normalizeSearchConditions(eventView.query);
  126. updateQuery(searchConditions, action, column, value);
  127. browserHistory.push({
  128. pathname: location.pathname,
  129. query: {
  130. ...location.query,
  131. cursor: undefined,
  132. query: searchConditions.formatString(),
  133. },
  134. });
  135. };
  136. }
  137. function handleTransactionsListSortChange(value: string) {
  138. const target = {
  139. pathname: location.pathname,
  140. query: {...location.query, showTransactions: value, transactionCursor: undefined},
  141. };
  142. browserHistory.push(target);
  143. }
  144. function handleAllEventsViewClick() {
  145. trackAnalytics('performance_views.summary.view_in_transaction_events', {
  146. organization,
  147. });
  148. }
  149. function generateEventView(
  150. transactionsListEventView: EventView,
  151. transactionsListTitles: string[]
  152. ) {
  153. const {selected} = getTransactionsListSort(location, {
  154. p95: totalValues?.['p95()'] ?? 0,
  155. spanOperationBreakdownFilter,
  156. });
  157. const sortedEventView = transactionsListEventView.withSorts([selected.sort]);
  158. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  159. const fields = [
  160. // Remove the extra field columns
  161. ...sortedEventView.fields.slice(0, transactionsListTitles.length),
  162. ];
  163. // omit "Operation Duration" column
  164. sortedEventView.fields = fields.filter(({field}) => {
  165. return !isRelativeSpanOperationBreakdownField(field);
  166. });
  167. }
  168. return sortedEventView;
  169. }
  170. function generateActionBarItems(
  171. _org: Organization,
  172. _location: Location,
  173. _mepDataContext: MetricsEnhancedPerformanceDataContext
  174. ) {
  175. let items: ActionBarItem[] | undefined = undefined;
  176. if (!canUseTransactionMetricsData(_org, _mepDataContext)) {
  177. items = [
  178. {
  179. key: 'alert',
  180. makeAction: () => ({
  181. Button: () => <MetricsWarningIcon />,
  182. menuItem: {
  183. key: 'alert',
  184. },
  185. }),
  186. },
  187. ];
  188. }
  189. return items;
  190. }
  191. const trailingItems = useMemo(() => {
  192. if (!canUseTransactionMetricsData(organization, mepDataContext)) {
  193. return <MetricsWarningIcon />;
  194. }
  195. return null;
  196. }, [organization, mepDataContext]);
  197. const hasPerformanceChartInterpolation = organization.features.includes(
  198. 'performance-chart-interpolation'
  199. );
  200. const query = useMemo(() => {
  201. return decodeScalar(location.query.query, '');
  202. }, [location]);
  203. const totalCount = totalValues === null ? null : totalValues['count()'];
  204. // NOTE: This is not a robust check for whether or not a transaction is a front end
  205. // transaction, however it will suffice for now.
  206. const hasWebVitals =
  207. isSummaryViewFrontendPageLoad(eventView, projects) ||
  208. (totalValues !== null &&
  209. VITAL_GROUPS.some(group =>
  210. group.vitals.some(vital => {
  211. const functionName = `percentile(${vital},${VITAL_PERCENTILE})`;
  212. const field = functionName;
  213. return Number.isFinite(totalValues[field]) && totalValues[field] !== 0;
  214. })
  215. ));
  216. const isFrontendView = isSummaryViewFrontend(eventView, projects);
  217. const transactionsListTitles = [
  218. t('event id'),
  219. t('user'),
  220. t('total duration'),
  221. t('trace id'),
  222. t('timestamp'),
  223. ];
  224. const project = projects.find(p => p.id === projectId);
  225. let transactionsListEventView = eventView.clone();
  226. const fields = [...transactionsListEventView.fields];
  227. if (
  228. organization.features.includes('session-replay') &&
  229. project &&
  230. projectSupportsReplay(project)
  231. ) {
  232. transactionsListTitles.push(t('replay'));
  233. fields.push({field: 'replayId'});
  234. }
  235. if (
  236. // only show for projects that already sent a profile
  237. // once we have a more compact design we will show this for
  238. // projects that support profiling as well
  239. project?.hasProfiles &&
  240. (organization.features.includes('profiling') ||
  241. organization.features.includes('continuous-profiling'))
  242. ) {
  243. transactionsListTitles.push(t('profile'));
  244. if (organization.features.includes('profiling')) {
  245. fields.push({field: 'profile.id'});
  246. }
  247. if (organization.features.includes('continuous-profiling')) {
  248. fields.push({field: 'profiler.id'});
  249. fields.push({field: 'thread.id'});
  250. fields.push({field: 'precise.start_ts'});
  251. fields.push({field: 'precise.finish_ts'});
  252. }
  253. }
  254. // update search conditions
  255. const spanOperationBreakdownConditions = filterToSearchConditions(
  256. spanOperationBreakdownFilter,
  257. location
  258. );
  259. if (spanOperationBreakdownConditions) {
  260. eventView = eventView.clone();
  261. eventView.query = `${eventView.query} ${spanOperationBreakdownConditions}`.trim();
  262. transactionsListEventView = eventView.clone();
  263. }
  264. // update header titles of transactions list
  265. const operationDurationTableTitle =
  266. spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE
  267. ? t('operation duration')
  268. : `${spanOperationBreakdownFilter} duration`;
  269. // add ops breakdown duration column as the 3rd column
  270. transactionsListTitles.splice(2, 0, operationDurationTableTitle);
  271. // span_ops_breakdown.relative is a preserved name and a marker for the associated
  272. // field renderer to be used to generate the relative ops breakdown
  273. let durationField = SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
  274. if (spanOperationBreakdownFilter !== SpanOperationBreakdownFilter.NONE) {
  275. durationField = filterToField(spanOperationBreakdownFilter)!;
  276. }
  277. // add ops breakdown duration column as the 3rd column
  278. fields.splice(2, 0, {field: durationField});
  279. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  280. fields.push(
  281. ...SPAN_OP_BREAKDOWN_FIELDS.map(field => {
  282. return {field};
  283. })
  284. );
  285. }
  286. transactionsListEventView.fields = fields;
  287. const openAllEventsProps = {
  288. generatePerformanceTransactionEventsView: () => {
  289. const performanceTransactionEventsView = generateEventView(
  290. transactionsListEventView,
  291. transactionsListTitles
  292. );
  293. performanceTransactionEventsView.query = query;
  294. return performanceTransactionEventsView;
  295. },
  296. handleOpenAllEventsClick: handleAllEventsViewClick,
  297. };
  298. const hasNewSpansUIFlag =
  299. organization.features.includes('performance-spans-new-ui') &&
  300. organization.features.includes('insights-initial-modules');
  301. const projectIds = useMemo(() => eventView.project.slice(), [eventView.project]);
  302. function renderSearchBar() {
  303. if (organization.features.includes('search-query-builder-performance')) {
  304. return (
  305. <TransactionSearchQueryBuilder
  306. projects={projectIds}
  307. initialQuery={query}
  308. onSearch={handleSearch}
  309. searchSource="transaction_summary"
  310. disableLoadingTags // already loaded by the parent component
  311. filterKeyMenuWidth={420}
  312. trailingItems={trailingItems}
  313. />
  314. );
  315. }
  316. return (
  317. <SearchBar
  318. searchSource="transaction_summary"
  319. organization={organization}
  320. projectIds={eventView.project}
  321. query={query}
  322. fields={eventView.fields}
  323. onSearch={handleSearch}
  324. maxQueryLength={MAX_QUERY_LENGTH}
  325. actionBarItems={generateActionBarItems(organization, location, mepDataContext)}
  326. />
  327. );
  328. }
  329. return (
  330. <Fragment>
  331. <Layout.Main>
  332. <FilterActions>
  333. <Filter
  334. organization={organization}
  335. currentFilter={spanOperationBreakdownFilter}
  336. onChangeFilter={onChangeFilter}
  337. />
  338. <PageFilterBar condensed>
  339. <EnvironmentPageFilter />
  340. <DatePageFilter />
  341. </PageFilterBar>
  342. <StyledSearchBarWrapper>{renderSearchBar()}</StyledSearchBarWrapper>
  343. </FilterActions>
  344. <PerformanceAtScaleContextProvider>
  345. <TransactionSummaryCharts
  346. organization={organization}
  347. location={location}
  348. eventView={eventView}
  349. totalValue={totalCount}
  350. currentFilter={spanOperationBreakdownFilter}
  351. withoutZerofill={hasPerformanceChartInterpolation}
  352. project={project}
  353. />
  354. <TransactionsList
  355. location={location}
  356. organization={organization}
  357. eventView={transactionsListEventView}
  358. {...openAllEventsProps}
  359. showTransactions={
  360. decodeScalar(
  361. location.query.showTransactions,
  362. TransactionFilterOptions.SLOW
  363. ) as TransactionFilterOptions
  364. }
  365. breakdown={decodeFilterFromLocation(location)}
  366. titles={transactionsListTitles}
  367. handleDropdownChange={handleTransactionsListSortChange}
  368. generateLink={{
  369. id: generateTransactionIdLink(transactionName),
  370. trace: generateTraceLink(eventView.normalizeDateSelection(location)),
  371. replayId: generateReplayLink(routes),
  372. 'profile.id': generateProfileLink(),
  373. }}
  374. handleCellAction={handleCellAction}
  375. {...getTransactionsListSort(location, {
  376. p95: totalValues?.['p95()'] ?? 0,
  377. spanOperationBreakdownFilter,
  378. })}
  379. forceLoading={isLoading}
  380. referrer="performance.transactions_summary"
  381. supportsInvestigationRule
  382. />
  383. </PerformanceAtScaleContextProvider>
  384. {!hasNewSpansUIFlag && (
  385. <SuspectSpans
  386. location={location}
  387. organization={organization}
  388. eventView={eventView}
  389. totals={
  390. defined(totalValues?.['count()'])
  391. ? {'count()': totalValues!['count()']}
  392. : null
  393. }
  394. projectId={projectId}
  395. transactionName={transactionName}
  396. />
  397. )}
  398. <TagExplorer
  399. eventView={eventView}
  400. organization={organization}
  401. location={location}
  402. projects={projects}
  403. transactionName={transactionName}
  404. currentFilter={spanOperationBreakdownFilter}
  405. />
  406. <SuspectFunctionsTable
  407. project={project}
  408. transaction={transactionName}
  409. analyticsPageSource="performance_transaction"
  410. />
  411. <RelatedIssues
  412. organization={organization}
  413. location={location}
  414. transaction={transactionName}
  415. start={eventView.start}
  416. end={eventView.end}
  417. statsPeriod={eventView.statsPeriod}
  418. />
  419. </Layout.Main>
  420. <Layout.Side>
  421. <UserStats
  422. organization={organization}
  423. location={location}
  424. isLoading={isLoading}
  425. hasWebVitals={hasWebVitals}
  426. error={error}
  427. totals={totalValues}
  428. transactionName={transactionName}
  429. eventView={eventView}
  430. />
  431. {!isFrontendView && (
  432. <StatusBreakdown
  433. eventView={eventView}
  434. organization={organization}
  435. location={location}
  436. />
  437. )}
  438. <SidebarSpacer />
  439. <SidebarCharts
  440. organization={organization}
  441. isLoading={isLoading}
  442. error={error}
  443. totals={totalValues}
  444. eventView={eventView}
  445. transactionName={transactionName}
  446. />
  447. <SidebarSpacer />
  448. <Tags
  449. generateUrl={generateTagUrl}
  450. totalValues={totalCount}
  451. eventView={eventView}
  452. organization={organization}
  453. location={location}
  454. />
  455. </Layout.Side>
  456. </Fragment>
  457. );
  458. }
  459. function getFilterOptions({
  460. p95,
  461. spanOperationBreakdownFilter,
  462. }: {
  463. p95: number;
  464. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  465. }): DropdownOption[] {
  466. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  467. return [
  468. {
  469. sort: {kind: 'asc', field: 'transaction.duration'},
  470. value: TransactionFilterOptions.FASTEST,
  471. label: t('Fastest Transactions'),
  472. },
  473. {
  474. query: p95 > 0 ? [['transaction.duration', `<=${p95.toFixed(0)}`]] : undefined,
  475. sort: {kind: 'desc', field: 'transaction.duration'},
  476. value: TransactionFilterOptions.SLOW,
  477. label: t('Slow Transactions (p95)'),
  478. },
  479. {
  480. sort: {kind: 'desc', field: 'transaction.duration'},
  481. value: TransactionFilterOptions.OUTLIER,
  482. label: t('Outlier Transactions (p100)'),
  483. },
  484. {
  485. sort: {kind: 'desc', field: 'timestamp'},
  486. value: TransactionFilterOptions.RECENT,
  487. label: t('Recent Transactions'),
  488. },
  489. ];
  490. }
  491. const field = filterToField(spanOperationBreakdownFilter)!;
  492. const operationName = spanOperationBreakdownFilter;
  493. return [
  494. {
  495. sort: {kind: 'asc', field},
  496. value: TransactionFilterOptions.FASTEST,
  497. label: t('Fastest %s Operations', operationName),
  498. },
  499. {
  500. query: p95 > 0 ? [['transaction.duration', `<=${p95.toFixed(0)}`]] : undefined,
  501. sort: {kind: 'desc', field},
  502. value: TransactionFilterOptions.SLOW,
  503. label: t('Slow %s Operations (p95)', operationName),
  504. },
  505. {
  506. sort: {kind: 'desc', field},
  507. value: TransactionFilterOptions.OUTLIER,
  508. label: t('Outlier %s Operations (p100)', operationName),
  509. },
  510. {
  511. sort: {kind: 'desc', field: 'timestamp'},
  512. value: TransactionFilterOptions.RECENT,
  513. label: t('Recent Transactions'),
  514. },
  515. ];
  516. }
  517. function getTransactionsListSort(
  518. location: Location,
  519. options: {p95: number; spanOperationBreakdownFilter: SpanOperationBreakdownFilter}
  520. ): {options: DropdownOption[]; selected: DropdownOption} {
  521. const sortOptions = getFilterOptions(options);
  522. const urlParam = decodeScalar(
  523. location.query.showTransactions,
  524. TransactionFilterOptions.SLOW
  525. );
  526. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  527. return {selected: selectedSort, options: sortOptions};
  528. }
  529. function MetricsWarningIcon() {
  530. return (
  531. <Tooltip
  532. title={t(
  533. 'Based on your search criteria and sample rate, the events available may be limited.'
  534. )}
  535. >
  536. <StyledIconWarning
  537. data-test-id="search-metrics-fallback-warning"
  538. size="sm"
  539. color="warningText"
  540. />
  541. </Tooltip>
  542. );
  543. }
  544. const FilterActions = styled('div')`
  545. display: grid;
  546. gap: ${space(2)};
  547. margin-bottom: ${space(2)};
  548. @media (min-width: ${p => p.theme.breakpoints.small}) {
  549. grid-template-columns: repeat(2, min-content);
  550. }
  551. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  552. grid-template-columns: auto auto 1fr;
  553. }
  554. `;
  555. const StyledSearchBarWrapper = styled('div')`
  556. @media (min-width: ${p => p.theme.breakpoints.small}) {
  557. order: 1;
  558. grid-column: 1/4;
  559. }
  560. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  561. order: initial;
  562. grid-column: auto;
  563. }
  564. `;
  565. const StyledIconWarning = styled(IconWarning)`
  566. display: block;
  567. `;
  568. export default withProjects(SummaryContent);