content.tsx 18 KB

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