content.tsx 19 KB

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