diff --git a/static/app/utils/performance/trends/trendsDiscoverQuery.tsx b/static/app/utils/performance/trends/trendsDiscoverQuery.tsx index 8ef90e27943f60..3903fcb0e22993 100644 --- a/static/app/utils/performance/trends/trendsDiscoverQuery.tsx +++ b/static/app/utils/performance/trends/trendsDiscoverQuery.tsx @@ -7,6 +7,7 @@ import GenericDiscoverQuery, { import withApi from 'app/utils/withApi'; import { TrendChangeType, + TrendFunctionField, TrendsData, TrendsDataEvents, TrendsQuery, @@ -20,17 +21,21 @@ import { export type TrendsRequest = { trendChangeType?: TrendChangeType; + trendFunctionField?: TrendFunctionField; eventView: Partial; }; type RequestProps = DiscoverQueryProps & TrendsRequest; -type ChildrenProps = Omit, 'tableData'> & { +export type TrendDiscoveryChildrenProps = Omit< + GenericChildrenProps, + 'tableData' +> & { trendsData: TrendsData | null; }; type Props = RequestProps & { - children: (props: ChildrenProps) => React.ReactNode; + children: (props: TrendDiscoveryChildrenProps) => React.ReactNode; }; type EventChildrenProps = Omit, 'tableData'> & { @@ -44,13 +49,13 @@ type EventProps = RequestProps & { export function getTrendsRequestPayload(props: RequestProps) { const {eventView} = props; const apiPayload: TrendsQuery = eventView?.getEventsAPIPayload(props.location); - const trendFunction = getCurrentTrendFunction(props.location); + const trendFunction = getCurrentTrendFunction(props.location, props.trendFunctionField); const trendParameter = getCurrentTrendParameter(props.location); apiPayload.trendFunction = generateTrendFunctionAsString( trendFunction.field, trendParameter.column ); - apiPayload.trendType = eventView?.trendType; + apiPayload.trendType = eventView?.trendType || props.trendChangeType; apiPayload.interval = eventView?.interval; apiPayload.middle = eventView?.middle; return apiPayload; @@ -59,9 +64,9 @@ export function getTrendsRequestPayload(props: RequestProps) { function TrendsDiscoverQuery(props: Props) { return ( + {...props} route="events-trends-stats" getRequestPayload={getTrendsRequestPayload} - {...props} > {({tableData, ...rest}) => { return props.children({trendsData: tableData, ...rest}); @@ -73,9 +78,9 @@ function TrendsDiscoverQuery(props: Props) { function EventsDiscoverQuery(props: EventProps) { return ( + {...props} route="events-trends" getRequestPayload={getTrendsRequestPayload} - {...props} > {({tableData, ...rest}) => { return props.children({trendsData: tableData, ...rest}); diff --git a/static/app/views/performance/content.tsx b/static/app/views/performance/content.tsx index 79be7520c0dfc5..d287d52ac18bc1 100644 --- a/static/app/views/performance/content.tsx +++ b/static/app/views/performance/content.tsx @@ -21,19 +21,16 @@ import {GlobalSelection, Organization, Project} from 'app/types'; import {trackAnalyticsEvent} from 'app/utils/analytics'; import EventView from 'app/utils/discover/eventView'; import {PerformanceEventViewProvider} from 'app/utils/performance/contexts/performanceEventViewContext'; -import {decodeScalar} from 'app/utils/queryString'; -import {MutableSearch} from 'app/utils/tokenizeSearch'; import withApi from 'app/utils/withApi'; import withGlobalSelection from 'app/utils/withGlobalSelection'; import withOrganization from 'app/utils/withOrganization'; import withProjects from 'app/utils/withProjects'; import LandingContent from './landing/content'; -import {DEFAULT_MAX_DURATION} from './trends/utils'; import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data'; import {PerformanceLanding} from './landing'; import Onboarding from './onboarding'; -import {addRoutePerformanceContext, getPerformanceTrendsUrl} from './utils'; +import {addRoutePerformanceContext, handleTrendsClick} from './utils'; type Props = { api: Client; @@ -131,49 +128,6 @@ class PerformanceContent extends Component { }); }; - handleTrendsClick = () => { - const {location, organization} = this.props; - - const newQuery = { - ...location.query, - }; - - const query = decodeScalar(location.query.query, ''); - const conditions = new MutableSearch(query); - - trackAnalyticsEvent({ - eventKey: 'performance_views.change_view', - eventName: 'Performance Views: Change View', - organization_id: parseInt(organization.id, 10), - view_name: 'TRENDS', - }); - - const modifiedConditions = new MutableSearch([]); - - if (conditions.hasFilter('tpm()')) { - modifiedConditions.setFilterValues('tpm()', conditions.getFilterValues('tpm()')); - } else { - modifiedConditions.setFilterValues('tpm()', ['>0.01']); - } - if (conditions.hasFilter('transaction.duration')) { - modifiedConditions.setFilterValues( - 'transaction.duration', - conditions.getFilterValues('transaction.duration') - ); - } else { - modifiedConditions.setFilterValues('transaction.duration', [ - '>0', - `<${DEFAULT_MAX_DURATION}`, - ]); - } - newQuery.query = modifiedConditions.formatString(); - - browserHistory.push({ - pathname: getPerformanceTrendsUrl(organization), - query: {...newQuery}, - }); - }; - shouldShowOnboarding() { const {projects, demoMode} = this.props; const {eventView} = this.state; @@ -218,7 +172,7 @@ class PerformanceContent extends Component { @@ -259,7 +213,7 @@ class PerformanceContent extends Component { eventView={this.state.eventView} setError={this.setError} handleSearch={this.handleSearch} - handleTrendsClick={this.handleTrendsClick} + handleTrendsClick={() => handleTrendsClick(this.props)} shouldShowOnboarding={this.shouldShowOnboarding()} {...this.props} /> diff --git a/static/app/views/performance/landing/utils.tsx b/static/app/views/performance/landing/utils.tsx index 24fc83fe9b16f8..7ec0c426f3121e 100644 --- a/static/app/views/performance/landing/utils.tsx +++ b/static/app/views/performance/landing/utils.tsx @@ -1,3 +1,4 @@ +import {ReactText} from 'react'; import {browserHistory} from 'react-router'; import {Location} from 'history'; @@ -60,6 +61,25 @@ export const LANDING_DISPLAYS = [ }, ]; +export function excludeTransaction( + transaction: string | ReactText, + props: {eventView: EventView; location: Location} +) { + const {eventView, location} = props; + + const searchConditions = new MutableSearch(eventView.query); + searchConditions.addFilterValues('!transaction', [`${transaction}`]); + + browserHistory.push({ + pathname: location.pathname, + query: { + ...location.query, + cursor: undefined, + query: searchConditions.formatString(), + }, + }); +} + export function getCurrentLandingDisplay( location: Location, projects: Project[], diff --git a/static/app/views/performance/landing/views/allTransactionsView.tsx b/static/app/views/performance/landing/views/allTransactionsView.tsx index d33aa984fccf67..89c0bc1fd54daa 100644 --- a/static/app/views/performance/landing/views/allTransactionsView.tsx +++ b/static/app/views/performance/landing/views/allTransactionsView.tsx @@ -24,9 +24,10 @@ export function AllTransactionsView(props: BasePerformanceViewProps) { diff --git a/static/app/views/performance/landing/widgets/components/performanceWidget.tsx b/static/app/views/performance/landing/widgets/components/performanceWidget.tsx index c0e8348e9ca9d2..0ca8129338052e 100644 --- a/static/app/views/performance/landing/widgets/components/performanceWidget.tsx +++ b/static/app/views/performance/landing/widgets/components/performanceWidget.tsx @@ -68,7 +68,7 @@ function _DataDisplay( props: GenericPerformanceWidgetProps & WidgetDataProps & {nextWidgetData: T; totalHeight: number} ) { - const {Visualizations, chartHeight, totalHeight, containerType} = props; + const {Visualizations, chartHeight, totalHeight, containerType, EmptyComponent} = props; const Container = getPerformanceWidgetContainer({ containerType, @@ -111,7 +111,14 @@ function _DataDisplay( /> ))} - emptyComponent={} + loadingComponent={} + emptyComponent={ + EmptyComponent ? ( + + ) : ( + + ) + } /> ); diff --git a/static/app/views/performance/landing/widgets/components/selectableList.tsx b/static/app/views/performance/landing/widgets/components/selectableList.tsx index 50ea77d6b3c357..fa2710da5c572e 100644 --- a/static/app/views/performance/landing/widgets/components/selectableList.tsx +++ b/static/app/views/performance/landing/widgets/components/selectableList.tsx @@ -53,10 +53,8 @@ export const RightAlignedCell = styled('div')` `; const ListItemContainer = styled('div')` - display: grid; - grid-template-columns: 24px auto 150px 30px; - grid-template-rows: repeat(2, auto); - grid-column-gap: ${space(1)}; + display: flex; + border-top: 1px solid ${p => p.theme.border}; padding: ${space(1)} ${space(2)}; `; diff --git a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx index 0b645619c6207c..282d1c5b88a549 100644 --- a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx +++ b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx @@ -12,6 +12,7 @@ import {GenericPerformanceWidgetDataType} from '../types'; import {PerformanceWidgetSetting, WIDGET_DEFINITIONS} from '../widgetDefinitions'; import {LineChartListWidget} from '../widgets/lineChartListWidget'; import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget'; +import {TrendsWidget} from '../widgets/trendsWidget'; import {ChartRowProps} from './widgetChartRow'; @@ -89,7 +90,7 @@ const _WidgetContainer = (props: Props) => { switch (widgetProps.dataType) { case GenericPerformanceWidgetDataType.trends: - throw new Error('Trends not currently supported.'); + return ; case GenericPerformanceWidgetDataType.area: return ; case GenericPerformanceWidgetDataType.line_list: diff --git a/static/app/views/performance/landing/widgets/transforms/transformTrendsDiscover.tsx b/static/app/views/performance/landing/widgets/transforms/transformTrendsDiscover.tsx new file mode 100644 index 00000000000000..c4767f90cb749c --- /dev/null +++ b/static/app/views/performance/landing/widgets/transforms/transformTrendsDiscover.tsx @@ -0,0 +1,21 @@ +import {TrendDiscoveryChildrenProps} from 'app/utils/performance/trends/trendsDiscoverQuery'; +import {normalizeTrends} from 'app/views/performance/trends/utils'; + +export function transformTrendsDiscover(_: any, props: TrendDiscoveryChildrenProps) { + const {trendsData} = props; + const events = trendsData + ? normalizeTrends((trendsData && trendsData.events && trendsData.events.data) || []) + : []; + return { + ...props, + data: trendsData, + hasData: !!trendsData?.events?.data.length, + loading: props.isLoading, + isLoading: props.isLoading, + isErrored: !!props.error, + errored: props.error, + statsData: trendsData ? trendsData.stats : {}, + transactionsList: events && events.slice ? events.slice(0, 3) : [], + events, + }; +} diff --git a/static/app/views/performance/landing/widgets/types.tsx b/static/app/views/performance/landing/widgets/types.tsx index 42b38a5216b5d0..a71bce61d8845e 100644 --- a/static/app/views/performance/landing/widgets/types.tsx +++ b/static/app/views/performance/landing/widgets/types.tsx @@ -110,6 +110,8 @@ export type GenericPerformanceWidgetProps = { // Components HeaderActions?: HeaderActions; + EmptyComponent?: FunctionComponent<{height?: number}>; + Queries: Queries; Visualizations: Visualizations; }; diff --git a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx index 5c54e8c23ba4f1..ae81598274417a 100644 --- a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx @@ -1,5 +1,5 @@ -import {Fragment, FunctionComponent, ReactText, useMemo, useState} from 'react'; -import {browserHistory, withRouter} from 'react-router'; +import {Fragment, FunctionComponent, useMemo, useState} from 'react'; +import {withRouter} from 'react-router'; import styled from '@emotion/styled'; import {Location} from 'history'; @@ -7,8 +7,10 @@ import _EventsRequest from 'app/components/charts/eventsRequest'; import {getInterval} from 'app/components/charts/utils'; import Link from 'app/components/links/link'; import Tooltip from 'app/components/tooltip'; +import Truncate from 'app/components/truncate'; import {IconClose} from 'app/icons'; import {t} from 'app/locale'; +import space from 'app/styles/space'; import {Organization} from 'app/types'; import DiscoverQuery from 'app/utils/discover/discoverQuery'; import EventView from 'app/utils/discover/eventView'; @@ -17,6 +19,7 @@ import withApi from 'app/utils/withApi'; import _DurationChart from 'app/views/performance/charts/chart'; import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils'; +import {excludeTransaction} from '../../utils'; import {GenericPerformanceWidget} from '../components/performanceWidget'; import SelectableList, {RightAlignedCell} from '../components/selectableList'; import {transformDiscoverToList} from '../transforms/transformDiscoverToList'; @@ -43,22 +46,6 @@ type DataType = { list: WidgetDataResult & ReturnType; }; -function excludeTransaction(transaction: string | ReactText, props: Props) { - const {eventView, location} = props; - - const searchConditions = new MutableSearch(eventView.query); - searchConditions.addFilterValues('!transaction', [`${transaction}`]); - - browserHistory.push({ - pathname: location.pathname, - query: { - ...location.query, - cursor: undefined, - query: searchConditions.formatString(), - }, - }); -} - export function LineChartListWidget(props: Props) { const [selectedListIndex, setSelectListIndex] = useState(0); const {ContainerActions} = props; @@ -190,17 +177,20 @@ export function LineChartListWidget(props: Props) { selectedIndex={selectedListIndex} setSelectedIndex={setSelectListIndex} items={provided.widgetData.list.data.map(listItem => () => { + const transaction = listItem.transaction as string; const transactionTarget = transactionSummaryRouteWithQuery({ orgSlug: props.organization.slug, projectID: listItem['project.id'] as string, - transaction: listItem.transaction as string, + transaction, query: props.eventView.getGlobalSelectionQuery(), }); switch (props.chartSetting) { case PerformanceWidgetSetting.MOST_RELATED_ISSUES: return ( - {listItem.transaction} + + + - {listItem.transaction} + + + {listItem.failure_count} ; +}; + +type TrendsWidgetDataType = { + chart: WidgetDataResult & ReturnType; +}; + +const fields = [{field: 'transaction'}, {field: 'project'}]; + +export function TrendsWidget(props: Props) { + const {eventView: _eventView, ContainerActions} = props; + const trendChangeType = + props.chartSetting === PerformanceWidgetSetting.MOST_IMPROVED + ? TrendChangeType.IMPROVED + : TrendChangeType.REGRESSION; + const trendFunctionField = TrendFunctionField.AVG; // Average is the easiest chart to understand. + + const [selectedListIndex, setSelectListIndex] = useState(0); + + const eventView = _eventView.clone(); + eventView.fields = fields; + eventView.sorts = [ + { + kind: trendChangeType === TrendChangeType.IMPROVED ? 'asc' : 'desc', + field: 'trend_percentage()', + }, + ]; + const rest = {...props, eventView}; + eventView.additionalConditions.addFilterValues('tpm()', ['>0.01']); + eventView.additionalConditions.addFilterValues('count_percentage()', ['>0.25', '<4']); + eventView.additionalConditions.addFilterValues('trend_percentage()', ['>0%']); + eventView.additionalConditions.addFilterValues('confidence()', ['>6']); + + const Queries = useMemo(() => { + return { + chart: { + fields: ['transaction', 'project'], + component: provided => ( + + ), + transform: transformTrendsDiscover, + }, + }; + }, [_eventView, trendChangeType]); + + return ( + + {...rest} + subtitle={{t('Trending Transactions')}} + HeaderActions={provided => } + Queries={Queries} + Visualizations={[ + { + component: provided => ( + + ), + bottomPadding: false, + height: 160, + }, + { + component: provided => ( + () => { + const initialConditions = new MutableSearch([]); + initialConditions.addFilterValues('transaction', [listItem.transaction]); + + const trendsTarget = trendsTargetRoute({ + organization: props.organization, + location: props.location, + initialConditions, + additionalQuery: { + trendFunction: trendFunctionField, + }, + }); + return ( + + + + + + + + + { + excludeTransaction(listItem.transaction, props); + setSelectListIndex(0); + }} + /> + + + ); + })} + /> + ), + height: 200, + noPadding: true, + }, + ]} + EmptyComponent={() => ( + {t('No results')} + )} + /> + ); +} + +const TrendsChart = withRouter(withProjects(Chart)); +const Subtitle = styled('span')` + color: ${p => p.theme.gray300}; + font-size: ${p => p.theme.fontSizeMedium}; +`; +const CloseContainer = styled('div')` + display: flex; + align-items: center; + justify-content: center; +`; +const GrowLink = styled(Link)` + flex-grow: 1; +`; + +const StyledIconClose = styled(IconClose)` + cursor: pointer; + color: ${p => p.theme.gray200}; + + &:hover { + color: ${p => p.theme.gray300}; + } +`; + +const StyledEmptyStateWarning = styled(EmptyStateWarning)` + min-height: 300px; + justify-content: center; +`; diff --git a/static/app/views/performance/trends/changedTransactions.tsx b/static/app/views/performance/trends/changedTransactions.tsx index ae1cfa4fd48eab..abe1bc828eb15f 100644 --- a/static/app/views/performance/trends/changedTransactions.tsx +++ b/static/app/views/performance/trends/changedTransactions.tsx @@ -486,7 +486,11 @@ function TrendsListItem(props: TrendsListItemProps) { ); } -const CompareDurations = ({transaction}: TrendsListItemProps) => { +export const CompareDurations = ({ + transaction, +}: { + transaction: TrendsListItemProps['transaction']; +}) => { const {fromSeconds, toSeconds, showDigits} = transformDeltaSpread( transaction.aggregate_range_1, transaction.aggregate_range_2 diff --git a/static/app/views/performance/trends/chart.tsx b/static/app/views/performance/trends/chart.tsx index ad03b43a0b5252..9aef3912b58d0b 100644 --- a/static/app/views/performance/trends/chart.tsx +++ b/static/app/views/performance/trends/chart.tsx @@ -17,7 +17,12 @@ import getDynamicText from 'app/utils/getDynamicText'; import {decodeList} from 'app/utils/queryString'; import {Theme} from 'app/utils/theme'; -import {NormalizedTrendsTransaction, TrendChangeType, TrendsStats} from './types'; +import { + NormalizedTrendsTransaction, + TrendChangeType, + TrendFunctionField, + TrendsStats, +} from './types'; import { generateTrendFunctionAsString, getCurrentTrendFunction, @@ -43,10 +48,15 @@ type Props = WithRouterProps & location: Location; organization: OrganizationSummary; trendChangeType: TrendChangeType; + trendFunctionField?: TrendFunctionField; transaction?: NormalizedTrendsTransaction; isLoading: boolean; statsData: TrendsStats; projects: Project[]; + height?: number; + grid?: LineChart['props']['grid']; + disableXAxis?: boolean; + disableLegend?: boolean; }; function transformEventStats(data: EventsStatsData, seriesName?: string): Series[] { @@ -222,7 +232,7 @@ function getIntervalLine( return additionalLineSeries; } -function Chart({ +export function Chart({ trendChangeType, router, statsPeriod, @@ -235,6 +245,11 @@ function Chart({ projects, start: propsStart, end: propsEnd, + trendFunctionField, + disableXAxis, + disableLegend, + grid, + height, }: Props) { const theme = useTheme(); @@ -264,7 +279,7 @@ function Chart({ : undefined; const data = events?.data ?? []; - const trendFunction = getCurrentTrendFunction(location); + const trendFunction = getCurrentTrendFunction(location, trendFunctionField); const trendParameter = getCurrentTrendParameter(location); const chartLabel = generateTrendFunctionAsString( trendFunction.field, @@ -286,10 +301,12 @@ function Chart({ selection[metric] = false; return selection; }, {}); - const legend = { - ...getLegend(chartLabel), - selected: seriesSelection, - }; + const legend = disableLegend + ? {show: false} + : { + ...getLegend(chartLabel), + selected: seriesSelection, + }; const loading = isLoading; const reloading = isLoading; @@ -379,6 +396,7 @@ function Chart({ {getDynamicText({ value: ( ), fixed: 'Duration Chart', diff --git a/static/app/views/performance/trends/utils.tsx b/static/app/views/performance/trends/utils.tsx index c6d97caa23117d..5bd0295f226077 100644 --- a/static/app/views/performance/trends/utils.tsx +++ b/static/app/views/performance/trends/utils.tsx @@ -135,8 +135,12 @@ export function resetCursors() { return cursors; } -export function getCurrentTrendFunction(location: Location): TrendFunction { - const trendFunctionField = decodeScalar(location?.query?.trendFunction); +export function getCurrentTrendFunction( + location: Location, + _trendFunctionField?: TrendFunctionField +): TrendFunction { + const trendFunctionField = + _trendFunctionField ?? decodeScalar(location?.query?.trendFunction); const trendFunction = TRENDS_FUNCTIONS.find(({field}) => field === trendFunctionField); return trendFunction || TRENDS_FUNCTIONS[0]; } diff --git a/static/app/views/performance/utils.tsx b/static/app/views/performance/utils.tsx index d7a7df4b8e452d..2c37f7e3591f0e 100644 --- a/static/app/views/performance/utils.tsx +++ b/static/app/views/performance/utils.tsx @@ -1,10 +1,12 @@ +import {browserHistory} from 'react-router'; import {Location, LocationDescriptor, Query} from 'history'; import Duration from 'app/components/duration'; import {ALL_ACCESS_PROJECTS} from 'app/constants/globalSelectionHeader'; import {backend, frontend, mobile} from 'app/data/platformCategories'; -import {GlobalSelection, OrganizationSummary, Project} from 'app/types'; +import {GlobalSelection, Organization, OrganizationSummary, Project} from 'app/types'; import {defined} from 'app/utils'; +import {trackAnalyticsEvent} from 'app/utils/analytics'; import {statsPeriodToDays} from 'app/utils/dates'; import EventView from 'app/utils/discover/eventView'; import {TRACING_FIELDS} from 'app/utils/discover/fields'; @@ -13,6 +15,8 @@ import getCurrentSentryReactTransaction from 'app/utils/getCurrentSentryReactTra import {decodeScalar} from 'app/utils/queryString'; import {MutableSearch} from 'app/utils/tokenizeSearch'; +import {DEFAULT_MAX_DURATION} from './trends/utils'; + /** * Performance type can used to determine a default view or which specific field should be used by default on pages * where we don't want to wait for transaction data to return to determine how to display aspects of a page. @@ -117,6 +121,67 @@ export function getTransactionSearchQuery(location: Location, query: string = '' return decodeScalar(location.query.query, query).trim(); } +export function handleTrendsClick({ + location, + organization, +}: { + location: Location; + organization: Organization; +}) { + trackAnalyticsEvent({ + eventKey: 'performance_views.change_view', + eventName: 'Performance Views: Change View', + organization_id: parseInt(organization.id, 10), + view_name: 'TRENDS', + }); + + const target = trendsTargetRoute({location, organization}); + + browserHistory.push(target); +} + +export function trendsTargetRoute({ + location, + organization, + initialConditions, + additionalQuery, +}: { + location: Location; + organization: Organization; + initialConditions?: MutableSearch; + additionalQuery?: {[x: string]: string}; +}) { + const newQuery = { + ...location.query, + ...additionalQuery, + }; + + const query = decodeScalar(location.query.query, ''); + const conditions = new MutableSearch(query); + + const modifiedConditions = initialConditions ?? new MutableSearch([]); + + if (conditions.hasFilter('tpm()')) { + modifiedConditions.setFilterValues('tpm()', conditions.getFilterValues('tpm()')); + } else { + modifiedConditions.setFilterValues('tpm()', ['>0.01']); + } + if (conditions.hasFilter('transaction.duration')) { + modifiedConditions.setFilterValues( + 'transaction.duration', + conditions.getFilterValues('transaction.duration') + ); + } else { + modifiedConditions.setFilterValues('transaction.duration', [ + '>0', + `<${DEFAULT_MAX_DURATION}`, + ]); + } + newQuery.query = modifiedConditions.formatString(); + + return {pathname: getPerformanceTrendsUrl(organization), query: {...newQuery}}; +} + export function removeTracingKeysFromSearch( currentFilter: MutableSearch, options: {excludeTagKeys: Set} = { diff --git a/tests/js/spec/views/performance/landing/index.spec.tsx b/tests/js/spec/views/performance/landing/index.spec.tsx index 32e81a80fc1df9..271d8eaa8c125e 100644 --- a/tests/js/spec/views/performance/landing/index.spec.tsx +++ b/tests/js/spec/views/performance/landing/index.spec.tsx @@ -157,8 +157,8 @@ describe('Performance > Landing > Index', function () { expect(wrapper.find('Table').exists()).toBe(true); - expect(eventStatsMock).toHaveBeenCalledTimes(4); // Currently defaulting to 4 event stat charts on all transactions view + 1 event chart. - expect(eventsV2Mock).toHaveBeenCalledTimes(1); + expect(eventStatsMock).toHaveBeenCalledTimes(3); // Currently defaulting to 4 event stat charts on all transactions view + 1 event chart. + expect(eventsV2Mock).toHaveBeenCalledTimes(2); const titles = wrapper.find('div[data-test-id="performance-widget-title"]'); expect(titles).toHaveLength(5); @@ -166,7 +166,7 @@ describe('Performance > Landing > Index', function () { expect(titles.at(0).text()).toEqual('User Misery'); expect(titles.at(1).text()).toEqual('Transactions Per Minute'); expect(titles.at(2).text()).toEqual('Failure Rate'); - expect(titles.at(3).text()).toEqual('Transactions Per Minute'); - expect(titles.at(4).text()).toEqual('Most Related Errors'); + expect(titles.at(3).text()).toEqual('Most Related Errors'); + expect(titles.at(4).text()).toEqual('Most Related Issues'); }); }); diff --git a/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx b/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx index 9d13ccf77cfe80..078e81950b631c 100644 --- a/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx +++ b/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx @@ -25,6 +25,7 @@ const WrappedComponent = ({data, ...rest}) => { describe('Performance > Widgets > WidgetContainer', function () { let eventStatsMock; let eventsV2Mock; + let eventsTrendsStats; beforeEach(function () { eventStatsMock = MockApiClient.addMockResponse({ method: 'GET', @@ -36,6 +37,11 @@ describe('Performance > Widgets > WidgetContainer', function () { url: `/organizations/org-slug/eventsv2/`, body: [], }); + eventsTrendsStats = MockApiClient.addMockResponse({ + method: 'GET', + url: '/organizations/org-slug/events-trends-stats/', + body: [], + }); }); it('TPM Widget', async function () { @@ -208,6 +214,84 @@ describe('Performance > Widgets > WidgetContainer', function () { ); }); + it('Most improved trends widget', async function () { + const data = initializeData(); + + const wrapper = mountWithTheme( + , + data.routerContext + ); + await tick(); + wrapper.update(); + + expect(wrapper.find('div[data-test-id="performance-widget-title"]').text()).toEqual( + 'Most Improved' + ); + expect(eventsTrendsStats).toHaveBeenCalledTimes(1); + expect(eventsTrendsStats).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ + query: expect.objectContaining({ + environment: [], + field: ['transaction', 'project'], + interval: undefined, + middle: undefined, + per_page: 3, + project: [], + query: + 'tpm():>0.01 count_percentage():>0.25 count_percentage():<4 trend_percentage():>0% confidence():>6', + sort: 'trend_percentage()', + statsPeriod: '14d', + trendFunction: 'avg(transaction.duration)', + trendType: 'improved', + }), + }) + ); + }); + + it('Most regressed trends widget', async function () { + const data = initializeData(); + + const wrapper = mountWithTheme( + , + data.routerContext + ); + await tick(); + wrapper.update(); + + expect(wrapper.find('div[data-test-id="performance-widget-title"]').text()).toEqual( + 'Most Regressed' + ); + expect(eventsTrendsStats).toHaveBeenCalledTimes(1); + expect(eventsTrendsStats).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ + query: expect.objectContaining({ + environment: [], + field: ['transaction', 'project'], + interval: undefined, + middle: undefined, + per_page: 3, + project: [], + query: + 'tpm():>0.01 count_percentage():>0.25 count_percentage():<4 trend_percentage():>0% confidence():>6', + sort: '-trend_percentage()', + statsPeriod: '14d', + trendFunction: 'avg(transaction.duration)', + trendType: 'regression', + }), + }) + ); + }); + it('Able to change widget type from menu', async function () { const data = initializeData();