123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- import {Component, Fragment} from 'react';
- import styled from '@emotion/styled';
- import type {Location} from 'history';
- import {Alert} from 'sentry/components/alert';
- import {Breadcrumbs} from 'sentry/components/breadcrumbs';
- import {CompactSelect} from 'sentry/components/compactSelect';
- import SearchBar from 'sentry/components/events/searchBar';
- import * as Layout from 'sentry/components/layouts/thirds';
- import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
- import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
- import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
- import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
- import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
- import TransactionNameSearchBar from 'sentry/components/performance/searchBar';
- import {MAX_QUERY_LENGTH} from 'sentry/constants';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {PageFilters} from 'sentry/types/core';
- import type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import type EventView from 'sentry/utils/discover/eventView';
- import {generateAggregateFields} from 'sentry/utils/discover/fields';
- import {decodeScalar} from 'sentry/utils/queryString';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import withPageFilters from 'sentry/utils/withPageFilters';
- import getSelectedQueryKey from 'sentry/views/performance/trends/utils/getSelectedQueryKey';
- import {getPerformanceLandingUrl, getTransactionSearchQuery} from '../utils';
- import ChangedTransactions from './changedTransactions';
- import type {TrendFunctionField, TrendView} from './types';
- import {TrendChangeType} from './types';
- import {
- DEFAULT_MAX_DURATION,
- DEFAULT_TRENDS_STATS_PERIOD,
- getCurrentTrendFunction,
- getCurrentTrendParameter,
- modifyTransactionNameTrendsQuery,
- modifyTrendsViewDefaultPeriod,
- resetCursors,
- TRENDS_FUNCTIONS,
- TRENDS_PARAMETERS,
- } from './utils';
- type Props = {
- eventView: EventView;
- location: Location;
- organization: Organization;
- projects: Project[];
- selection: PageFilters;
- };
- type State = {
- error?: string;
- previousTrendFunction?: TrendFunctionField;
- };
- export const defaultTrendsSelectionDate = {
- start: null,
- end: null,
- utc: false,
- period: DEFAULT_TRENDS_STATS_PERIOD,
- };
- class TrendsContent extends Component<Props, State> {
- state: State = {};
- handleSearch = (searchQuery: string) => {
- const {location} = this.props;
- const cursors = resetCursors();
- browserHistory.push({
- pathname: location.pathname,
- query: {
- ...location.query,
- ...cursors,
- query: String(searchQuery).trim() || undefined,
- },
- });
- };
- setError = (error: string | undefined) => {
- this.setState({error});
- };
- handleTrendFunctionChange = (field: string) => {
- const {organization, location} = this.props;
- const offsets = {};
- Object.values(TrendChangeType).forEach(trendChangeType => {
- const queryKey = getSelectedQueryKey(trendChangeType);
- offsets[queryKey] = undefined;
- });
- trackAnalytics('performance_views.trends.change_function', {
- organization,
- function_name: field,
- });
- this.setState({
- previousTrendFunction: getCurrentTrendFunction(location).field,
- });
- const cursors = resetCursors();
- browserHistory.push({
- pathname: location.pathname,
- query: {
- ...location.query,
- ...offsets,
- ...cursors,
- trendFunction: field,
- },
- });
- };
- renderError() {
- const {error} = this.state;
- if (!error) {
- return null;
- }
- return (
- <Alert type="error" showIcon>
- {error}
- </Alert>
- );
- }
- handleParameterChange = (label: string) => {
- const {organization, location} = this.props;
- const cursors = resetCursors();
- trackAnalytics('performance_views.trends.change_parameter', {
- organization,
- parameter_name: label,
- });
- browserHistory.push({
- pathname: location.pathname,
- query: {
- ...location.query,
- ...cursors,
- trendParameter: label,
- },
- });
- };
- getFreeTextFromQuery(query: string) {
- const conditions = new MutableSearch(query);
- const transactionValues = conditions.getFilterValues('transaction');
- if (transactionValues.length) {
- return transactionValues[0];
- }
- if (conditions.freeText.length > 0) {
- // raw text query will be wrapped in wildcards in generatePerformanceEventView
- // so no need to wrap it here
- return conditions.freeText.join(' ');
- }
- return '';
- }
- getPerformanceLink() {
- const {location} = this.props;
- const newQuery = {
- ...location.query,
- };
- const query = decodeScalar(location.query.query, '');
- const conditions = new MutableSearch(query);
- // This stops errors from occurring when navigating to other views since we are appending aggregates to the trends view
- conditions.removeFilter('tpm()');
- conditions.removeFilter('confidence()');
- conditions.removeFilter('transaction.duration');
- newQuery.query = conditions.formatString();
- return {
- pathname: getPerformanceLandingUrl(this.props.organization),
- query: newQuery,
- };
- }
- render() {
- const {organization, eventView, location, projects} = this.props;
- const {previousTrendFunction} = this.state;
- const trendView = eventView.clone() as TrendView;
- modifyTrendsViewDefaultPeriod(trendView, location);
- if (organization.features.includes('performance-new-trends')) {
- modifyTransactionNameTrendsQuery(trendView);
- }
- const fields = generateAggregateFields(
- organization,
- [
- {
- field: 'absolute_correlation()',
- },
- {
- field: 'trend_percentage()',
- },
- {
- field: 'trend_difference()',
- },
- {
- field: 'count_percentage()',
- },
- {
- field: 'tpm()',
- },
- {
- field: 'tps()',
- },
- ],
- ['epm()', 'eps()']
- );
- const currentTrendFunction = getCurrentTrendFunction(location);
- const currentTrendParameter = getCurrentTrendParameter(
- location,
- projects,
- eventView.project
- );
- const query = getTransactionSearchQuery(location);
- return (
- <PageFiltersContainer
- defaultSelection={{
- datetime: defaultTrendsSelectionDate,
- }}
- >
- <Layout.Header>
- <Layout.HeaderContent>
- <Breadcrumbs
- crumbs={[
- {
- label: 'Performance',
- to: this.getPerformanceLink(),
- },
- {
- label: 'Trends',
- },
- ]}
- />
- <Layout.Title>{t('Trends')}</Layout.Title>
- </Layout.HeaderContent>
- </Layout.Header>
- <Layout.Body>
- <Layout.Main fullWidth>
- <DefaultTrends location={location} eventView={eventView} projects={projects}>
- <FilterActions>
- <PageFilterBar condensed>
- <ProjectPageFilter />
- <EnvironmentPageFilter />
- <DatePageFilter />
- </PageFilterBar>
- {organization.features.includes('performance-new-trends') ? (
- <StyledTransactionNameSearchBar
- organization={organization}
- eventView={trendView}
- onSearch={this.handleSearch}
- query={this.getFreeTextFromQuery(query)}
- />
- ) : (
- <StyledSearchBar
- searchSource="trends"
- organization={organization}
- projectIds={trendView.project}
- query={query}
- fields={fields}
- onSearch={this.handleSearch}
- maxQueryLength={MAX_QUERY_LENGTH}
- />
- )}
- <CompactSelect
- triggerProps={{prefix: t('Percentile')}}
- value={currentTrendFunction.field}
- options={TRENDS_FUNCTIONS.map(({label, field}) => ({
- value: field,
- label,
- }))}
- onChange={opt => this.handleTrendFunctionChange(opt.value)}
- />
- <CompactSelect
- triggerProps={{prefix: t('Parameter')}}
- value={currentTrendParameter.label}
- options={TRENDS_PARAMETERS.map(({label}) => ({
- value: label,
- label,
- }))}
- onChange={opt => this.handleParameterChange(opt.value)}
- />
- </FilterActions>
- <ListContainer>
- <ChangedTransactions
- trendChangeType={TrendChangeType.IMPROVED}
- previousTrendFunction={previousTrendFunction}
- trendView={trendView}
- location={location}
- setError={this.setError}
- withBreakpoint={organization.features.includes(
- 'performance-new-trends'
- )}
- />
- <ChangedTransactions
- trendChangeType={TrendChangeType.REGRESSION}
- previousTrendFunction={previousTrendFunction}
- trendView={trendView}
- location={location}
- setError={this.setError}
- withBreakpoint={organization.features.includes(
- 'performance-new-trends'
- )}
- />
- </ListContainer>
- </DefaultTrends>
- </Layout.Main>
- </Layout.Body>
- </PageFiltersContainer>
- );
- }
- }
- type DefaultTrendsProps = {
- children: React.ReactNode[];
- eventView: EventView;
- location: Location;
- projects: Project[];
- };
- class DefaultTrends extends Component<DefaultTrendsProps> {
- hasPushedDefaults = false;
- render() {
- const {children, location, eventView, projects} = this.props;
- const queryString = decodeScalar(location.query.query);
- const trendParameter = getCurrentTrendParameter(
- location,
- projects,
- eventView.project
- );
- const conditions = new MutableSearch(queryString || '');
- if (queryString || this.hasPushedDefaults) {
- this.hasPushedDefaults = true;
- return <Fragment>{children}</Fragment>;
- }
- this.hasPushedDefaults = true;
- conditions.setFilterValues('tpm()', ['>0.01']);
- conditions.setFilterValues(trendParameter.column, ['>0', `<${DEFAULT_MAX_DURATION}`]);
- const query = conditions.formatString();
- eventView.query = query;
- browserHistory.push({
- pathname: location.pathname,
- query: {
- ...location.query,
- cursor: undefined,
- query: String(query).trim() || undefined,
- },
- });
- return null;
- }
- }
- const FilterActions = styled('div')`
- display: grid;
- gap: ${space(2)};
- margin-bottom: ${space(2)};
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- grid-template-columns: repeat(3, min-content);
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- grid-template-columns: auto 1fr auto auto;
- }
- `;
- const StyledSearchBar = styled(SearchBar)`
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- order: 1;
- grid-column: 1/5;
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- order: initial;
- grid-column: auto;
- }
- `;
- const StyledTransactionNameSearchBar = styled(TransactionNameSearchBar)`
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- order: 1;
- grid-column: 1/5;
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- order: initial;
- grid-column: auto;
- }
- `;
- const ListContainer = styled('div')`
- display: grid;
- gap: ${space(2)};
- margin-bottom: ${space(2)};
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- `;
- export default withPageFilters(TrendsContent);
|