content.tsx 18 KB

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