content.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import {Fragment} 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 {SuspectFunctionsTable} from 'sentry/components/profiling/suspectFunctions/suspectFunctionsTable';
  14. import type {ActionBarItem} from 'sentry/components/smartSearchBar';
  15. import {Tooltip} from 'sentry/components/tooltip';
  16. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  17. import {IconWarning} from 'sentry/icons';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {Organization} from 'sentry/types/organization';
  21. import type {Project} from 'sentry/types/project';
  22. import {defined, generateQueryWithTag} from 'sentry/utils';
  23. import {trackAnalytics} from 'sentry/utils/analytics';
  24. import {browserHistory} from 'sentry/utils/browserHistory';
  25. import type EventView from 'sentry/utils/discover/eventView';
  26. import {
  27. formatTagKey,
  28. isRelativeSpanOperationBreakdownField,
  29. SPAN_OP_BREAKDOWN_FIELDS,
  30. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  31. } from 'sentry/utils/discover/fields';
  32. import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
  33. import type {MetricsEnhancedPerformanceDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  34. import {useMEPDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  35. import {decodeScalar} from 'sentry/utils/queryString';
  36. import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
  37. import {useRoutes} from 'sentry/utils/useRoutes';
  38. import withProjects from 'sentry/utils/withProjects';
  39. import type {Actions} from 'sentry/views/discover/table/cellAction';
  40. import {updateQuery} from 'sentry/views/discover/table/cellAction';
  41. import type {TableColumn} from 'sentry/views/discover/table/types';
  42. import Tags from 'sentry/views/discover/tags';
  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. generateTransactionIdLink,
  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. };
  85. function SummaryContent({
  86. eventView,
  87. location,
  88. totalValues,
  89. spanOperationBreakdownFilter,
  90. organization,
  91. projects,
  92. isLoading,
  93. error,
  94. projectId,
  95. transactionName,
  96. onChangeFilter,
  97. }: Props) {
  98. const routes = useRoutes();
  99. const mepDataContext = useMEPDataContext();
  100. function handleSearch(query: string) {
  101. const queryParams = normalizeDateTimeParams({
  102. ...(location.query || {}),
  103. query,
  104. });
  105. // do not propagate pagination when making a new search
  106. const searchQueryParams = omit(queryParams, 'cursor');
  107. browserHistory.push({
  108. pathname: location.pathname,
  109. query: searchQueryParams,
  110. });
  111. }
  112. function generateTagUrl(key: string, value: string) {
  113. const query = generateQueryWithTag(location.query, {key: formatTagKey(key), value});
  114. return {
  115. ...location,
  116. query,
  117. };
  118. }
  119. function handleCellAction(column: TableColumn<React.ReactText>) {
  120. return (action: Actions, value: React.ReactText) => {
  121. const searchConditions = normalizeSearchConditions(eventView.query);
  122. updateQuery(searchConditions, action, column, value);
  123. browserHistory.push({
  124. pathname: location.pathname,
  125. query: {
  126. ...location.query,
  127. cursor: undefined,
  128. query: searchConditions.formatString(),
  129. },
  130. });
  131. };
  132. }
  133. function handleTransactionsListSortChange(value: string) {
  134. const target = {
  135. pathname: location.pathname,
  136. query: {...location.query, showTransactions: value, transactionCursor: undefined},
  137. };
  138. browserHistory.push(target);
  139. }
  140. function handleAllEventsViewClick() {
  141. trackAnalytics('performance_views.summary.view_in_transaction_events', {
  142. organization,
  143. });
  144. }
  145. function generateEventView(
  146. transactionsListEventView: EventView,
  147. transactionsListTitles: string[]
  148. ) {
  149. const {selected} = getTransactionsListSort(location, {
  150. p95: totalValues?.['p95()'] ?? 0,
  151. spanOperationBreakdownFilter,
  152. });
  153. const sortedEventView = transactionsListEventView.withSorts([selected.sort]);
  154. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  155. const fields = [
  156. // Remove the extra field columns
  157. ...sortedEventView.fields.slice(0, transactionsListTitles.length),
  158. ];
  159. // omit "Operation Duration" column
  160. sortedEventView.fields = fields.filter(({field}) => {
  161. return !isRelativeSpanOperationBreakdownField(field);
  162. });
  163. }
  164. return sortedEventView;
  165. }
  166. function generateActionBarItems(
  167. _org: Organization,
  168. _location: Location,
  169. _mepDataContext: MetricsEnhancedPerformanceDataContext
  170. ) {
  171. let items: ActionBarItem[] | undefined = undefined;
  172. if (!canUseTransactionMetricsData(_org, _mepDataContext)) {
  173. items = [
  174. {
  175. key: 'alert',
  176. makeAction: () => ({
  177. Button: () => (
  178. <Tooltip
  179. title={t(
  180. 'Based on your search criteria and sample rate, the events available may be limited.'
  181. )}
  182. >
  183. <StyledIconWarning
  184. data-test-id="search-metrics-fallback-warning"
  185. size="sm"
  186. color="warningText"
  187. />
  188. </Tooltip>
  189. ),
  190. menuItem: {
  191. key: 'alert',
  192. },
  193. }),
  194. },
  195. ];
  196. }
  197. return items;
  198. }
  199. const hasPerformanceChartInterpolation = organization.features.includes(
  200. 'performance-chart-interpolation'
  201. );
  202. const query = decodeScalar(location.query.query, '');
  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. organization.features.includes('profiling') &&
  237. project &&
  238. // only show for projects that already sent a profile
  239. // once we have a more compact design we will show this for
  240. // projects that support profiling as well
  241. project.hasProfiles
  242. ) {
  243. transactionsListTitles.push(t('profile'));
  244. fields.push({field: 'profile.id'});
  245. }
  246. // update search conditions
  247. const spanOperationBreakdownConditions = filterToSearchConditions(
  248. spanOperationBreakdownFilter,
  249. location
  250. );
  251. if (spanOperationBreakdownConditions) {
  252. eventView = eventView.clone();
  253. eventView.query = `${eventView.query} ${spanOperationBreakdownConditions}`.trim();
  254. transactionsListEventView = eventView.clone();
  255. }
  256. // update header titles of transactions list
  257. const operationDurationTableTitle =
  258. spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE
  259. ? t('operation duration')
  260. : `${spanOperationBreakdownFilter} duration`;
  261. // add ops breakdown duration column as the 3rd column
  262. transactionsListTitles.splice(2, 0, operationDurationTableTitle);
  263. // span_ops_breakdown.relative is a preserved name and a marker for the associated
  264. // field renderer to be used to generate the relative ops breakdown
  265. let durationField = SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
  266. if (spanOperationBreakdownFilter !== SpanOperationBreakdownFilter.NONE) {
  267. durationField = filterToField(spanOperationBreakdownFilter)!;
  268. }
  269. // add ops breakdown duration column as the 3rd column
  270. fields.splice(2, 0, {field: durationField});
  271. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  272. fields.push(
  273. ...SPAN_OP_BREAKDOWN_FIELDS.map(field => {
  274. return {field};
  275. })
  276. );
  277. }
  278. transactionsListEventView.fields = fields;
  279. const openAllEventsProps = {
  280. generatePerformanceTransactionEventsView: () => {
  281. const performanceTransactionEventsView = generateEventView(
  282. transactionsListEventView,
  283. transactionsListTitles
  284. );
  285. performanceTransactionEventsView.query = query;
  286. return performanceTransactionEventsView;
  287. },
  288. handleOpenAllEventsClick: handleAllEventsViewClick,
  289. };
  290. return (
  291. <Fragment>
  292. <Layout.Main>
  293. <FilterActions>
  294. <Filter
  295. organization={organization}
  296. currentFilter={spanOperationBreakdownFilter}
  297. onChangeFilter={onChangeFilter}
  298. />
  299. <PageFilterBar condensed>
  300. <EnvironmentPageFilter />
  301. <DatePageFilter />
  302. </PageFilterBar>
  303. <StyledSearchBar
  304. searchSource="transaction_summary"
  305. organization={organization}
  306. projectIds={eventView.project}
  307. query={query}
  308. fields={eventView.fields}
  309. onSearch={handleSearch}
  310. maxQueryLength={MAX_QUERY_LENGTH}
  311. actionBarItems={generateActionBarItems(
  312. organization,
  313. location,
  314. mepDataContext
  315. )}
  316. />
  317. </FilterActions>
  318. <PerformanceAtScaleContextProvider>
  319. <TransactionSummaryCharts
  320. organization={organization}
  321. location={location}
  322. eventView={eventView}
  323. totalValue={totalCount}
  324. currentFilter={spanOperationBreakdownFilter}
  325. withoutZerofill={hasPerformanceChartInterpolation}
  326. project={project}
  327. />
  328. <TransactionsList
  329. location={location}
  330. organization={organization}
  331. eventView={transactionsListEventView}
  332. {...openAllEventsProps}
  333. showTransactions={
  334. decodeScalar(
  335. location.query.showTransactions,
  336. TransactionFilterOptions.SLOW
  337. ) as TransactionFilterOptions
  338. }
  339. breakdown={decodeFilterFromLocation(location)}
  340. titles={transactionsListTitles}
  341. handleDropdownChange={handleTransactionsListSortChange}
  342. generateLink={{
  343. id: generateTransactionIdLink(transactionName),
  344. trace: generateTraceLink(eventView.normalizeDateSelection(location)),
  345. replayId: generateReplayLink(routes),
  346. 'profile.id': generateProfileLink(),
  347. }}
  348. handleCellAction={handleCellAction}
  349. {...getTransactionsListSort(location, {
  350. p95: totalValues?.['p95()'] ?? 0,
  351. spanOperationBreakdownFilter,
  352. })}
  353. forceLoading={isLoading}
  354. referrer="performance.transactions_summary"
  355. supportsInvestigationRule
  356. />
  357. </PerformanceAtScaleContextProvider>
  358. <SuspectSpans
  359. location={location}
  360. organization={organization}
  361. eventView={eventView}
  362. totals={
  363. defined(totalValues?.['count()'])
  364. ? {'count()': totalValues!['count()']}
  365. : null
  366. }
  367. projectId={projectId}
  368. transactionName={transactionName}
  369. />
  370. <TagExplorer
  371. eventView={eventView}
  372. organization={organization}
  373. location={location}
  374. projects={projects}
  375. transactionName={transactionName}
  376. currentFilter={spanOperationBreakdownFilter}
  377. />
  378. <SuspectFunctionsTable
  379. project={project}
  380. transaction={transactionName}
  381. analyticsPageSource="performance_transaction"
  382. />
  383. <RelatedIssues
  384. organization={organization}
  385. location={location}
  386. transaction={transactionName}
  387. start={eventView.start}
  388. end={eventView.end}
  389. statsPeriod={eventView.statsPeriod}
  390. />
  391. </Layout.Main>
  392. <Layout.Side>
  393. <UserStats
  394. organization={organization}
  395. location={location}
  396. isLoading={isLoading}
  397. hasWebVitals={hasWebVitals}
  398. error={error}
  399. totals={totalValues}
  400. transactionName={transactionName}
  401. eventView={eventView}
  402. />
  403. {!isFrontendView && (
  404. <StatusBreakdown
  405. eventView={eventView}
  406. organization={organization}
  407. location={location}
  408. />
  409. )}
  410. <SidebarSpacer />
  411. <SidebarCharts
  412. organization={organization}
  413. isLoading={isLoading}
  414. error={error}
  415. totals={totalValues}
  416. eventView={eventView}
  417. transactionName={transactionName}
  418. />
  419. <SidebarSpacer />
  420. <Tags
  421. generateUrl={generateTagUrl}
  422. totalValues={totalCount}
  423. eventView={eventView}
  424. organization={organization}
  425. location={location}
  426. />
  427. </Layout.Side>
  428. </Fragment>
  429. );
  430. }
  431. function getFilterOptions({
  432. p95,
  433. spanOperationBreakdownFilter,
  434. }: {
  435. p95: number;
  436. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  437. }): DropdownOption[] {
  438. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  439. return [
  440. {
  441. sort: {kind: 'asc', field: 'transaction.duration'},
  442. value: TransactionFilterOptions.FASTEST,
  443. label: t('Fastest Transactions'),
  444. },
  445. {
  446. query: [['transaction.duration', `<=${p95.toFixed(0)}`]],
  447. sort: {kind: 'desc', field: 'transaction.duration'},
  448. value: TransactionFilterOptions.SLOW,
  449. label: t('Slow Transactions (p95)'),
  450. },
  451. {
  452. sort: {kind: 'desc', field: 'transaction.duration'},
  453. value: TransactionFilterOptions.OUTLIER,
  454. label: t('Outlier Transactions (p100)'),
  455. },
  456. {
  457. sort: {kind: 'desc', field: 'timestamp'},
  458. value: TransactionFilterOptions.RECENT,
  459. label: t('Recent Transactions'),
  460. },
  461. ];
  462. }
  463. const field = filterToField(spanOperationBreakdownFilter)!;
  464. const operationName = spanOperationBreakdownFilter;
  465. return [
  466. {
  467. sort: {kind: 'asc', field},
  468. value: TransactionFilterOptions.FASTEST,
  469. label: t('Fastest %s Operations', operationName),
  470. },
  471. {
  472. query: [['transaction.duration', `<=${p95.toFixed(0)}`]],
  473. sort: {kind: 'desc', field},
  474. value: TransactionFilterOptions.SLOW,
  475. label: t('Slow %s Operations (p95)', operationName),
  476. },
  477. {
  478. sort: {kind: 'desc', field},
  479. value: TransactionFilterOptions.OUTLIER,
  480. label: t('Outlier %s Operations (p100)', operationName),
  481. },
  482. {
  483. sort: {kind: 'desc', field: 'timestamp'},
  484. value: TransactionFilterOptions.RECENT,
  485. label: t('Recent Transactions'),
  486. },
  487. ];
  488. }
  489. function getTransactionsListSort(
  490. location: Location,
  491. options: {p95: number; spanOperationBreakdownFilter: SpanOperationBreakdownFilter}
  492. ): {options: DropdownOption[]; selected: DropdownOption} {
  493. const sortOptions = getFilterOptions(options);
  494. const urlParam = decodeScalar(
  495. location.query.showTransactions,
  496. TransactionFilterOptions.SLOW
  497. );
  498. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  499. return {selected: selectedSort, options: sortOptions};
  500. }
  501. const FilterActions = styled('div')`
  502. display: grid;
  503. gap: ${space(2)};
  504. margin-bottom: ${space(2)};
  505. @media (min-width: ${p => p.theme.breakpoints.small}) {
  506. grid-template-columns: repeat(2, min-content);
  507. }
  508. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  509. grid-template-columns: auto auto 1fr;
  510. }
  511. `;
  512. const StyledSearchBar = styled(SearchBar)`
  513. @media (min-width: ${p => p.theme.breakpoints.small}) {
  514. order: 1;
  515. grid-column: 1/4;
  516. }
  517. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  518. order: initial;
  519. grid-column: auto;
  520. }
  521. `;
  522. const StyledIconWarning = styled(IconWarning)`
  523. display: block;
  524. `;
  525. export default withProjects(SummaryContent);