content.tsx 18 KB

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