content.tsx 16 KB

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