content.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. import {Fragment, useCallback, useMemo} 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 {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
  14. import {SuspectFunctionsTable} from 'sentry/components/profiling/suspectFunctions/suspectFunctionsTable';
  15. import type {ActionBarItem} from 'sentry/components/smartSearchBar';
  16. import {Tooltip} from 'sentry/components/tooltip';
  17. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  18. import {IconWarning} from 'sentry/icons';
  19. import {t} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  21. import type {Organization} from 'sentry/types/organization';
  22. import type {Project} from 'sentry/types/project';
  23. import {defined, generateQueryWithTag} from 'sentry/utils';
  24. import {trackAnalytics} from 'sentry/utils/analytics';
  25. import {browserHistory} from 'sentry/utils/browserHistory';
  26. import type EventView from 'sentry/utils/discover/eventView';
  27. import {
  28. formatTagKey,
  29. isRelativeSpanOperationBreakdownField,
  30. SPAN_OP_BREAKDOWN_FIELDS,
  31. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  32. } from 'sentry/utils/discover/fields';
  33. import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
  34. import type {MetricsEnhancedPerformanceDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  35. import {useMEPDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  36. import {decodeScalar} from 'sentry/utils/queryString';
  37. import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
  38. import {useRoutes} from 'sentry/utils/useRoutes';
  39. import withProjects from 'sentry/utils/withProjects';
  40. import type {Actions} from 'sentry/views/discover/table/cellAction';
  41. import {updateQuery} from 'sentry/views/discover/table/cellAction';
  42. import type {TableColumn} from 'sentry/views/discover/table/types';
  43. import Tags from 'sentry/views/discover/tags';
  44. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  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. generateTransactionIdLink,
  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. };
  87. function SummaryContent({
  88. eventView,
  89. location,
  90. totalValues,
  91. spanOperationBreakdownFilter,
  92. organization,
  93. projects,
  94. isLoading,
  95. error,
  96. projectId,
  97. transactionName,
  98. onChangeFilter,
  99. }: Props) {
  100. const routes = useRoutes();
  101. const mepDataContext = useMEPDataContext();
  102. const domainViewFilters = useDomainViewFilters();
  103. const handleSearch = useCallback(
  104. (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. [location]
  117. );
  118. function generateTagUrl(key: string, value: string) {
  119. const query = generateQueryWithTag(location.query, {key: formatTagKey(key), value});
  120. return {
  121. ...location,
  122. query,
  123. };
  124. }
  125. function handleCellAction(column: TableColumn<React.ReactText>) {
  126. return (action: Actions, value: React.ReactText) => {
  127. const searchConditions = normalizeSearchConditions(eventView.query);
  128. updateQuery(searchConditions, action, column, value);
  129. browserHistory.push({
  130. pathname: location.pathname,
  131. query: {
  132. ...location.query,
  133. cursor: undefined,
  134. query: searchConditions.formatString(),
  135. },
  136. });
  137. };
  138. }
  139. function handleTransactionsListSortChange(value: string) {
  140. const target = {
  141. pathname: location.pathname,
  142. query: {...location.query, showTransactions: value, transactionCursor: undefined},
  143. };
  144. browserHistory.push(target);
  145. }
  146. function handleAllEventsViewClick() {
  147. trackAnalytics('performance_views.summary.view_in_transaction_events', {
  148. organization,
  149. });
  150. }
  151. function generateEventView(
  152. transactionsListEventView: EventView,
  153. transactionsListTitles: string[]
  154. ) {
  155. const {selected} = getTransactionsListSort(location, {
  156. p95: totalValues?.['p95()'] ?? 0,
  157. spanOperationBreakdownFilter,
  158. });
  159. const sortedEventView = transactionsListEventView.withSorts([selected.sort]);
  160. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  161. const fields = [
  162. // Remove the extra field columns
  163. ...sortedEventView.fields.slice(0, transactionsListTitles.length),
  164. ];
  165. // omit "Operation Duration" column
  166. sortedEventView.fields = fields.filter(({field}) => {
  167. return !isRelativeSpanOperationBreakdownField(field);
  168. });
  169. }
  170. return sortedEventView;
  171. }
  172. function generateActionBarItems(
  173. _org: Organization,
  174. _location: Location,
  175. _mepDataContext: MetricsEnhancedPerformanceDataContext
  176. ) {
  177. let items: ActionBarItem[] | undefined = undefined;
  178. if (!canUseTransactionMetricsData(_org, _mepDataContext)) {
  179. items = [
  180. {
  181. key: 'alert',
  182. makeAction: () => ({
  183. Button: () => <MetricsWarningIcon />,
  184. menuItem: {
  185. key: 'alert',
  186. },
  187. }),
  188. },
  189. ];
  190. }
  191. return items;
  192. }
  193. const trailingItems = useMemo(() => {
  194. if (!canUseTransactionMetricsData(organization, mepDataContext)) {
  195. return <MetricsWarningIcon />;
  196. }
  197. return null;
  198. }, [organization, mepDataContext]);
  199. const hasPerformanceChartInterpolation = organization.features.includes(
  200. 'performance-chart-interpolation'
  201. );
  202. const query = useMemo(() => {
  203. return decodeScalar(location.query.query, '');
  204. }, [location]);
  205. const totalCount = totalValues === null ? null : totalValues['count()'];
  206. // NOTE: This is not a robust check for whether or not a transaction is a front end
  207. // transaction, however it will suffice for now.
  208. const hasWebVitals =
  209. isSummaryViewFrontendPageLoad(eventView, projects) ||
  210. (totalValues !== null &&
  211. VITAL_GROUPS.some(group =>
  212. group.vitals.some(vital => {
  213. const functionName = `percentile(${vital},${VITAL_PERCENTILE})`;
  214. const field = functionName;
  215. return Number.isFinite(totalValues[field]) && totalValues[field] !== 0;
  216. })
  217. ));
  218. const isFrontendView = isSummaryViewFrontend(eventView, projects);
  219. const transactionsListTitles = [
  220. t('event id'),
  221. t('user'),
  222. t('total duration'),
  223. t('trace id'),
  224. t('timestamp'),
  225. ];
  226. const project = projects.find(p => p.id === projectId);
  227. let transactionsListEventView = eventView.clone();
  228. const fields = [...transactionsListEventView.fields];
  229. if (
  230. organization.features.includes('session-replay') &&
  231. project &&
  232. projectSupportsReplay(project)
  233. ) {
  234. transactionsListTitles.push(t('replay'));
  235. fields.push({field: 'replayId'});
  236. }
  237. if (
  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. (organization.features.includes('profiling') ||
  243. organization.features.includes('continuous-profiling'))
  244. ) {
  245. transactionsListTitles.push(t('profile'));
  246. if (organization.features.includes('profiling')) {
  247. fields.push({field: 'profile.id'});
  248. }
  249. if (organization.features.includes('continuous-profiling')) {
  250. fields.push({field: 'profiler.id'});
  251. fields.push({field: 'thread.id'});
  252. fields.push({field: 'precise.start_ts'});
  253. fields.push({field: 'precise.finish_ts'});
  254. }
  255. }
  256. // update search conditions
  257. const spanOperationBreakdownConditions = filterToSearchConditions(
  258. spanOperationBreakdownFilter,
  259. location
  260. );
  261. if (spanOperationBreakdownConditions) {
  262. eventView = eventView.clone();
  263. eventView.query = `${eventView.query} ${spanOperationBreakdownConditions}`.trim();
  264. transactionsListEventView = eventView.clone();
  265. }
  266. // update header titles of transactions list
  267. const operationDurationTableTitle =
  268. spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE
  269. ? t('operation duration')
  270. : `${spanOperationBreakdownFilter} duration`;
  271. // add ops breakdown duration column as the 3rd column
  272. transactionsListTitles.splice(2, 0, operationDurationTableTitle);
  273. // span_ops_breakdown.relative is a preserved name and a marker for the associated
  274. // field renderer to be used to generate the relative ops breakdown
  275. let durationField = SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
  276. if (spanOperationBreakdownFilter !== SpanOperationBreakdownFilter.NONE) {
  277. durationField = filterToField(spanOperationBreakdownFilter)!;
  278. }
  279. // add ops breakdown duration column as the 3rd column
  280. fields.splice(2, 0, {field: durationField});
  281. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  282. fields.push(
  283. ...SPAN_OP_BREAKDOWN_FIELDS.map(field => {
  284. return {field};
  285. })
  286. );
  287. }
  288. transactionsListEventView.fields = fields;
  289. const openAllEventsProps = {
  290. generatePerformanceTransactionEventsView: () => {
  291. const performanceTransactionEventsView = generateEventView(
  292. transactionsListEventView,
  293. transactionsListTitles
  294. );
  295. performanceTransactionEventsView.query = query;
  296. return performanceTransactionEventsView;
  297. },
  298. handleOpenAllEventsClick: handleAllEventsViewClick,
  299. };
  300. const hasNewSpansUIFlag =
  301. organization.features.includes('performance-spans-new-ui') &&
  302. organization.features.includes('insights-initial-modules');
  303. const projectIds = useMemo(() => eventView.project.slice(), [eventView.project]);
  304. function renderSearchBar() {
  305. if (organization.features.includes('search-query-builder-performance')) {
  306. return (
  307. <TransactionSearchQueryBuilder
  308. projects={projectIds}
  309. initialQuery={query}
  310. onSearch={handleSearch}
  311. searchSource="transaction_summary"
  312. disableLoadingTags // already loaded by the parent component
  313. filterKeyMenuWidth={420}
  314. trailingItems={trailingItems}
  315. />
  316. );
  317. }
  318. return (
  319. <SearchBar
  320. searchSource="transaction_summary"
  321. organization={organization}
  322. projectIds={eventView.project}
  323. query={query}
  324. fields={eventView.fields}
  325. onSearch={handleSearch}
  326. maxQueryLength={MAX_QUERY_LENGTH}
  327. actionBarItems={generateActionBarItems(organization, location, mepDataContext)}
  328. />
  329. );
  330. }
  331. return (
  332. <Fragment>
  333. <Layout.Main>
  334. <FilterActions>
  335. <Filter
  336. organization={organization}
  337. currentFilter={spanOperationBreakdownFilter}
  338. onChangeFilter={onChangeFilter}
  339. />
  340. <PageFilterBar condensed>
  341. <EnvironmentPageFilter />
  342. <DatePageFilter />
  343. </PageFilterBar>
  344. <StyledSearchBarWrapper>{renderSearchBar()}</StyledSearchBarWrapper>
  345. </FilterActions>
  346. <PerformanceAtScaleContextProvider>
  347. <TransactionSummaryCharts
  348. organization={organization}
  349. location={location}
  350. eventView={eventView}
  351. totalValue={totalCount}
  352. currentFilter={spanOperationBreakdownFilter}
  353. withoutZerofill={hasPerformanceChartInterpolation}
  354. project={project}
  355. />
  356. <TransactionsList
  357. location={location}
  358. organization={organization}
  359. eventView={transactionsListEventView}
  360. {...openAllEventsProps}
  361. showTransactions={
  362. decodeScalar(
  363. location.query.showTransactions,
  364. TransactionFilterOptions.SLOW
  365. ) as TransactionFilterOptions
  366. }
  367. breakdown={decodeFilterFromLocation(location)}
  368. titles={transactionsListTitles}
  369. handleDropdownChange={handleTransactionsListSortChange}
  370. generateLink={{
  371. id: generateTransactionIdLink(transactionName, domainViewFilters.view),
  372. trace: generateTraceLink(
  373. eventView.normalizeDateSelection(location),
  374. domainViewFilters.view
  375. ),
  376. replayId: generateReplayLink(routes),
  377. 'profile.id': generateProfileLink(),
  378. }}
  379. handleCellAction={handleCellAction}
  380. {...getTransactionsListSort(location, {
  381. p95: totalValues?.['p95()'] ?? 0,
  382. spanOperationBreakdownFilter,
  383. })}
  384. domainViewFilters={domainViewFilters}
  385. forceLoading={isLoading}
  386. referrer="performance.transactions_summary"
  387. supportsInvestigationRule
  388. />
  389. </PerformanceAtScaleContextProvider>
  390. {!hasNewSpansUIFlag && (
  391. <SuspectSpans
  392. location={location}
  393. organization={organization}
  394. eventView={eventView}
  395. totals={
  396. defined(totalValues?.['count()'])
  397. ? {'count()': totalValues!['count()']}
  398. : null
  399. }
  400. projectId={projectId}
  401. transactionName={transactionName}
  402. />
  403. )}
  404. <TagExplorer
  405. eventView={eventView}
  406. organization={organization}
  407. location={location}
  408. projects={projects}
  409. transactionName={transactionName}
  410. currentFilter={spanOperationBreakdownFilter}
  411. domainViewFilters={domainViewFilters}
  412. />
  413. <SuspectFunctionsTable
  414. project={project}
  415. transaction={transactionName}
  416. analyticsPageSource="performance_transaction"
  417. />
  418. <RelatedIssues
  419. organization={organization}
  420. location={location}
  421. transaction={transactionName}
  422. start={eventView.start}
  423. end={eventView.end}
  424. statsPeriod={eventView.statsPeriod}
  425. />
  426. </Layout.Main>
  427. <Layout.Side>
  428. <UserStats
  429. organization={organization}
  430. location={location}
  431. isLoading={isLoading}
  432. hasWebVitals={hasWebVitals}
  433. error={error}
  434. totals={totalValues}
  435. transactionName={transactionName}
  436. eventView={eventView}
  437. />
  438. {!isFrontendView && (
  439. <StatusBreakdown
  440. eventView={eventView}
  441. organization={organization}
  442. location={location}
  443. />
  444. )}
  445. <SidebarSpacer />
  446. <SidebarCharts
  447. organization={organization}
  448. isLoading={isLoading}
  449. error={error}
  450. totals={totalValues}
  451. eventView={eventView}
  452. transactionName={transactionName}
  453. />
  454. <SidebarSpacer />
  455. <Tags
  456. generateUrl={generateTagUrl}
  457. totalValues={totalCount}
  458. eventView={eventView}
  459. organization={organization}
  460. location={location}
  461. />
  462. </Layout.Side>
  463. </Fragment>
  464. );
  465. }
  466. function getFilterOptions({
  467. p95,
  468. spanOperationBreakdownFilter,
  469. }: {
  470. p95: number;
  471. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  472. }): DropdownOption[] {
  473. if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
  474. return [
  475. {
  476. sort: {kind: 'asc', field: 'transaction.duration'},
  477. value: TransactionFilterOptions.FASTEST,
  478. label: t('Fastest Transactions'),
  479. },
  480. {
  481. query: p95 > 0 ? [['transaction.duration', `<=${p95.toFixed(0)}`]] : undefined,
  482. sort: {kind: 'desc', field: 'transaction.duration'},
  483. value: TransactionFilterOptions.SLOW,
  484. label: t('Slow Transactions (p95)'),
  485. },
  486. {
  487. sort: {kind: 'desc', field: 'transaction.duration'},
  488. value: TransactionFilterOptions.OUTLIER,
  489. label: t('Outlier Transactions (p100)'),
  490. },
  491. {
  492. sort: {kind: 'desc', field: 'timestamp'},
  493. value: TransactionFilterOptions.RECENT,
  494. label: t('Recent Transactions'),
  495. },
  496. ];
  497. }
  498. const field = filterToField(spanOperationBreakdownFilter)!;
  499. const operationName = spanOperationBreakdownFilter;
  500. return [
  501. {
  502. sort: {kind: 'asc', field},
  503. value: TransactionFilterOptions.FASTEST,
  504. label: t('Fastest %s Operations', operationName),
  505. },
  506. {
  507. query: p95 > 0 ? [['transaction.duration', `<=${p95.toFixed(0)}`]] : undefined,
  508. sort: {kind: 'desc', field},
  509. value: TransactionFilterOptions.SLOW,
  510. label: t('Slow %s Operations (p95)', operationName),
  511. },
  512. {
  513. sort: {kind: 'desc', field},
  514. value: TransactionFilterOptions.OUTLIER,
  515. label: t('Outlier %s Operations (p100)', operationName),
  516. },
  517. {
  518. sort: {kind: 'desc', field: 'timestamp'},
  519. value: TransactionFilterOptions.RECENT,
  520. label: t('Recent Transactions'),
  521. },
  522. ];
  523. }
  524. function getTransactionsListSort(
  525. location: Location,
  526. options: {p95: number; spanOperationBreakdownFilter: SpanOperationBreakdownFilter}
  527. ): {options: DropdownOption[]; selected: DropdownOption} {
  528. const sortOptions = getFilterOptions(options);
  529. const urlParam = decodeScalar(
  530. location.query.showTransactions,
  531. TransactionFilterOptions.SLOW
  532. );
  533. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  534. return {selected: selectedSort, options: sortOptions};
  535. }
  536. function MetricsWarningIcon() {
  537. return (
  538. <Tooltip
  539. title={t(
  540. 'Based on your search criteria and sample rate, the events available may be limited.'
  541. )}
  542. >
  543. <StyledIconWarning
  544. data-test-id="search-metrics-fallback-warning"
  545. size="sm"
  546. color="warningText"
  547. />
  548. </Tooltip>
  549. );
  550. }
  551. const FilterActions = styled('div')`
  552. display: grid;
  553. gap: ${space(2)};
  554. margin-bottom: ${space(2)};
  555. @media (min-width: ${p => p.theme.breakpoints.small}) {
  556. grid-template-columns: repeat(2, min-content);
  557. }
  558. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  559. grid-template-columns: auto auto 1fr;
  560. }
  561. `;
  562. const StyledSearchBarWrapper = styled('div')`
  563. @media (min-width: ${p => p.theme.breakpoints.small}) {
  564. order: 1;
  565. grid-column: 1/4;
  566. }
  567. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  568. order: initial;
  569. grid-column: auto;
  570. }
  571. `;
  572. const StyledIconWarning = styled(IconWarning)`
  573. display: block;
  574. `;
  575. export default withProjects(SummaryContent);