content.tsx 18 KB

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