content.tsx 17 KB

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