content.tsx 16 KB

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