content.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  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. const hasTransactionSummaryCleanupFlag = organization.features.includes(
  293. 'performance-transaction-summary-cleanup'
  294. );
  295. return (
  296. <Fragment>
  297. <Layout.Main>
  298. <FilterActions
  299. hasTransactionSummaryCleanupFlag={hasTransactionSummaryCleanupFlag}
  300. >
  301. {!hasTransactionSummaryCleanupFlag && (
  302. <Filter
  303. organization={organization}
  304. currentFilter={spanOperationBreakdownFilter}
  305. onChangeFilter={onChangeFilter}
  306. />
  307. )}
  308. <PageFilterBar condensed>
  309. <EnvironmentPageFilter />
  310. <DatePageFilter />
  311. </PageFilterBar>
  312. <StyledSearchBar
  313. searchSource="transaction_summary"
  314. organization={organization}
  315. projectIds={eventView.project}
  316. query={query}
  317. fields={eventView.fields}
  318. onSearch={handleSearch}
  319. maxQueryLength={MAX_QUERY_LENGTH}
  320. actionBarItems={generateActionBarItems(
  321. organization,
  322. location,
  323. mepDataContext
  324. )}
  325. />
  326. </FilterActions>
  327. <PerformanceAtScaleContextProvider>
  328. <TransactionSummaryCharts
  329. organization={organization}
  330. location={location}
  331. eventView={eventView}
  332. totalValue={totalCount}
  333. currentFilter={spanOperationBreakdownFilter}
  334. withoutZerofill={hasPerformanceChartInterpolation}
  335. project={project}
  336. />
  337. <TransactionsList
  338. location={location}
  339. organization={organization}
  340. eventView={transactionsListEventView}
  341. {...openAllEventsProps}
  342. showTransactions={
  343. decodeScalar(
  344. location.query.showTransactions,
  345. TransactionFilterOptions.SLOW
  346. ) as TransactionFilterOptions
  347. }
  348. breakdown={decodeFilterFromLocation(location)}
  349. titles={transactionsListTitles}
  350. handleDropdownChange={handleTransactionsListSortChange}
  351. generateLink={{
  352. id: generateTransactionLink(transactionName),
  353. trace: generateTraceLink(eventView.normalizeDateSelection(location)),
  354. replayId: generateReplayLink(routes),
  355. 'profile.id': generateProfileLink(),
  356. }}
  357. handleCellAction={handleCellAction}
  358. {...getTransactionsListSort(location, {
  359. p95: totalValues?.['p95()'] ?? 0,
  360. spanOperationBreakdownFilter,
  361. })}
  362. forceLoading={isLoading}
  363. referrer="performance.transactions_summary"
  364. supportsInvestigationRule
  365. />
  366. </PerformanceAtScaleContextProvider>
  367. {!hasTransactionSummaryCleanupFlag && (
  368. <SuspectSpans
  369. location={location}
  370. organization={organization}
  371. eventView={eventView}
  372. totals={
  373. defined(totalValues?.['count()'])
  374. ? {'count()': totalValues!['count()']}
  375. : null
  376. }
  377. projectId={projectId}
  378. transactionName={transactionName}
  379. />
  380. )}
  381. <TagExplorer
  382. eventView={eventView}
  383. organization={organization}
  384. location={location}
  385. projects={projects}
  386. transactionName={transactionName}
  387. currentFilter={spanOperationBreakdownFilter}
  388. />
  389. <SuspectFunctionsTable
  390. project={project}
  391. transaction={transactionName}
  392. analyticsPageSource="performance_transaction"
  393. />
  394. <RelatedIssues
  395. organization={organization}
  396. location={location}
  397. transaction={transactionName}
  398. start={eventView.start}
  399. end={eventView.end}
  400. statsPeriod={eventView.statsPeriod}
  401. />
  402. </Layout.Main>
  403. <Layout.Side>
  404. <TransactionPercentage
  405. isLoading={isLoading}
  406. error={error}
  407. totals={totalValues}
  408. unfilteredTotals={unfilteredTotalValues}
  409. />
  410. <UserStats
  411. organization={organization}
  412. location={location}
  413. isLoading={isLoading}
  414. hasWebVitals={hasWebVitals}
  415. error={error}
  416. totals={totalValues}
  417. transactionName={transactionName}
  418. eventView={eventView}
  419. />
  420. {!isFrontendView && (
  421. <StatusBreakdown
  422. eventView={eventView}
  423. organization={organization}
  424. location={location}
  425. />
  426. )}
  427. <SidebarSpacer />
  428. <SidebarCharts
  429. organization={organization}
  430. isLoading={isLoading}
  431. error={error}
  432. totals={totalValues}
  433. eventView={eventView}
  434. transactionName={transactionName}
  435. />
  436. <SidebarSpacer />
  437. <Tags
  438. generateUrl={generateTagUrl}
  439. totalValues={totalCount}
  440. eventView={eventView}
  441. organization={organization}
  442. location={location}
  443. />
  444. </Layout.Side>
  445. </Fragment>
  446. );
  447. }
  448. function getFilterOptions({
  449. p95,
  450. spanOperationBreakdownFilter,
  451. }: {
  452. p95: number;
  453. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  454. }): DropdownOption[] {
  455. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  456. return [
  457. {
  458. sort: {kind: 'asc', field: 'transaction.duration'},
  459. value: TransactionFilterOptions.FASTEST,
  460. label: t('Fastest Transactions'),
  461. },
  462. {
  463. query: [['transaction.duration', `<=${p95.toFixed(0)}`]],
  464. sort: {kind: 'desc', field: 'transaction.duration'},
  465. value: TransactionFilterOptions.SLOW,
  466. label: t('Slow Transactions (p95)'),
  467. },
  468. {
  469. sort: {kind: 'desc', field: 'transaction.duration'},
  470. value: TransactionFilterOptions.OUTLIER,
  471. label: t('Outlier Transactions (p100)'),
  472. },
  473. {
  474. sort: {kind: 'desc', field: 'timestamp'},
  475. value: TransactionFilterOptions.RECENT,
  476. label: t('Recent Transactions'),
  477. },
  478. ];
  479. }
  480. const field = filterToField(spanOperationBreakdownFilter)!;
  481. const operationName = spanOperationBreakdownFilter;
  482. return [
  483. {
  484. sort: {kind: 'asc', field},
  485. value: TransactionFilterOptions.FASTEST,
  486. label: t('Fastest %s Operations', operationName),
  487. },
  488. {
  489. query: [['transaction.duration', `<=${p95.toFixed(0)}`]],
  490. sort: {kind: 'desc', field},
  491. value: TransactionFilterOptions.SLOW,
  492. label: t('Slow %s Operations (p95)', operationName),
  493. },
  494. {
  495. sort: {kind: 'desc', field},
  496. value: TransactionFilterOptions.OUTLIER,
  497. label: t('Outlier %s Operations (p100)', operationName),
  498. },
  499. {
  500. sort: {kind: 'desc', field: 'timestamp'},
  501. value: TransactionFilterOptions.RECENT,
  502. label: t('Recent Transactions'),
  503. },
  504. ];
  505. }
  506. function getTransactionsListSort(
  507. location: Location,
  508. options: {p95: number; spanOperationBreakdownFilter: SpanOperationBreakdownFilter}
  509. ): {options: DropdownOption[]; selected: DropdownOption} {
  510. const sortOptions = getFilterOptions(options);
  511. const urlParam = decodeScalar(
  512. location.query.showTransactions,
  513. TransactionFilterOptions.SLOW
  514. );
  515. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  516. return {selected: selectedSort, options: sortOptions};
  517. }
  518. const FilterActions = styled('div')<{hasTransactionSummaryCleanupFlag: boolean}>`
  519. display: grid;
  520. gap: ${space(2)};
  521. margin-bottom: ${space(2)};
  522. @media (min-width: ${p => p.theme.breakpoints.small}) {
  523. grid-template-columns: repeat(2, min-content);
  524. }
  525. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  526. ${p =>
  527. p.hasTransactionSummaryCleanupFlag
  528. ? `grid-template-columns: auto 1fr;`
  529. : `grid-template-columns: auto auto 1fr;`}
  530. }
  531. `;
  532. const StyledSearchBar = styled(SearchBar)`
  533. @media (min-width: ${p => p.theme.breakpoints.small}) {
  534. order: 1;
  535. grid-column: 1/4;
  536. }
  537. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  538. order: initial;
  539. grid-column: auto;
  540. }
  541. `;
  542. const StyledIconWarning = styled(IconWarning)`
  543. display: block;
  544. `;
  545. export default withProjects(SummaryContent);