diff --git a/static/app/views/performance/data.tsx b/static/app/views/performance/data.tsx index 254ba70a2a2f23..92b2f47872628d 100644 --- a/static/app/views/performance/data.tsx +++ b/static/app/views/performance/data.tsx @@ -50,6 +50,8 @@ export enum PERFORMANCE_TERM { SLOW_FRAMES = 'slowFrames', FROZEN_FRAMES = 'frozenFrames', STALL_PERCENTAGE = 'stallPercentage', + MOST_ISSUES = 'mostIssues', + MOST_ERRORS = 'mostErrors', } export type TooltipOption = SelectValue & { @@ -357,6 +359,8 @@ export const PERFORMANCE_TERMS: Record = { t('Warm start is a measure of the application start up time while still in memory.'), slowFrames: () => t('The count of the number of slow frames in the transaction.'), frozenFrames: () => t('The count of the number of frozen frames in the transaction.'), + mostErrors: () => t('Transactions with the most associated errors.'), + mostIssues: () => t('The most instances of an issue for a related transaction.'), stallPercentage: () => t( 'The percentage of the transaction duration in which the application is in a stalled state.' diff --git a/static/app/views/performance/landing/views/allTransactionsView.tsx b/static/app/views/performance/landing/views/allTransactionsView.tsx index 7220ae2877e069..d33aa984fccf67 100644 --- a/static/app/views/performance/landing/views/allTransactionsView.tsx +++ b/static/app/views/performance/landing/views/allTransactionsView.tsx @@ -24,12 +24,9 @@ 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 22f7c18ec7d18c..640cf91d7199b7 100644 --- a/static/app/views/performance/landing/widgets/components/performanceWidget.tsx +++ b/static/app/views/performance/landing/widgets/components/performanceWidget.tsx @@ -26,16 +26,21 @@ export function GenericPerformanceWidget( props: WidgetPropUnion ) { const [widgetData, setWidgetData] = useState({} as T); + const [nextWidgetData, setNextWidgetData] = useState({} as T); const setWidgetDataForKey = useCallback( (dataKey: string, result?: WidgetDataResult) => { if (result) { + setNextWidgetData({...nextWidgetData, [dataKey]: result}); + } + if (result?.hasData || result?.isErrored) { + setNextWidgetData({...nextWidgetData, [dataKey]: result}); setWidgetData({...widgetData, [dataKey]: result}); } }, - [setWidgetData] + [widgetData, nextWidgetData, setWidgetData, setNextWidgetData] ); - const widgetProps = {widgetData, setWidgetDataForKey}; + const widgetProps = {widgetData, nextWidgetData, setWidgetDataForKey}; const queries = Object.entries(props.Queries).map(([key, definition]) => ({ ...definition, @@ -44,6 +49,8 @@ export function GenericPerformanceWidget( const api = useApi(); + const totalHeight = props.Visualizations.reduce((acc, curr) => acc + curr.height, 0); + return ( ( queries={queries} api={api} /> - <_DataDisplay {...props} {...widgetProps} /> + <_DataDisplay {...props} {...widgetProps} totalHeight={totalHeight} /> ); } function _DataDisplay( - props: GenericPerformanceWidgetProps & WidgetDataProps + props: GenericPerformanceWidgetProps & + WidgetDataProps & {nextWidgetData: T; totalHeight: number} ) { - const {Visualizations, chartHeight, containerType} = props; + const {Visualizations, chartHeight, totalHeight, containerType} = props; const Container = getPerformanceWidgetContainer({ containerType, }); - const missingDataKeys = !Object.values(props.widgetData).length; + const numberKeys = Object.keys(props.Queries).length; + const missingDataKeys = Object.values(props.widgetData).length !== numberKeys; + const missingNextDataKeys = Object.values(props.nextWidgetData).length !== numberKeys; const hasData = !missingDataKeys && Object.values(props.widgetData).every(d => !d || d.hasData); const isLoading = - !missingDataKeys && Object.values(props.widgetData).some(d => !d || d.isLoading); + !missingNextDataKeys && + Object.values(props.nextWidgetData).some(d => !d || d.isLoading); const isErrored = !missingDataKeys && Object.values(props.widgetData).some(d => d && d.isErrored); + const paddingOffset = 32; // space(2) * 2; + return ( @@ -84,7 +97,7 @@ function _DataDisplay( isLoading={isLoading} isErrored={isErrored} hasData={hasData} - errorComponent={} + errorComponent={} dataComponents={Visualizations.map((Visualization, index) => ( ( /> ))} - emptyComponent={} + emptyComponent={} /> ); diff --git a/static/app/views/performance/landing/widgets/components/queryHandler.tsx b/static/app/views/performance/landing/widgets/components/queryHandler.tsx index ad8931ac80e80b..da20b7672cec39 100644 --- a/static/app/views/performance/landing/widgets/components/queryHandler.tsx +++ b/static/app/views/performance/landing/widgets/components/queryHandler.tsx @@ -39,6 +39,11 @@ export function QueryHandler( project={globalSelection.projects} environment={globalSelection.environments} organization={props.queryProps.organization} + orgSlug={props.queryProps.organization.slug} + query={props.queryProps.eventView.getQueryWithAdditionalConditions()} + eventView={props.queryProps.eventView} + location={props.queryProps.location} + widgetData={props.widgetData} > {results => { return ( diff --git a/static/app/views/performance/landing/widgets/components/selectableList.tsx b/static/app/views/performance/landing/widgets/components/selectableList.tsx new file mode 100644 index 00000000000000..50ea77d6b3c357 --- /dev/null +++ b/static/app/views/performance/landing/widgets/components/selectableList.tsx @@ -0,0 +1,72 @@ +import React, {ReactNode} from 'react'; +import styled from '@emotion/styled'; + +import Radio from 'app/components/radio'; +import space from 'app/styles/space'; +import {RadioLineItem} from 'app/views/settings/components/forms/controls/radioGroup'; + +type Props = { + selectedIndex: number; + setSelectedIndex: (index: number) => void; + items: (() => ReactNode)[]; + radioColor?: string; +}; + +export default function SelectableList(props: Props) { + return ( +
+ {props.items.map((item, index) => ( + + {item()} + + ))} +
+ ); +} + +function SelectableItem({ + isSelected, + currentIndex: index, + children, + setSelectedIndex, + radioColor, +}: {isSelected: boolean; currentIndex: number; children: React.ReactNode} & Props) { + return ( + + + + setSelectedIndex(index)} /> + + + {children} + + ); +} + +export const RightAlignedCell = styled('div')` + text-align: right; +`; + +const ListItemContainer = styled('div')` + display: grid; + grid-template-columns: 24px auto 150px 30px; + grid-template-rows: repeat(2, auto); + grid-column-gap: ${space(1)}; + border-top: 1px solid ${p => p.theme.border}; + padding: ${space(1)} ${space(2)}; +`; + +const ItemRadioContainer = styled('div')` + grid-row: 1/3; + input { + cursor: pointer; + } + input:checked::after { + background-color: ${p => p.color}; + } +`; diff --git a/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx b/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx index 83fb65397158c4..3800c240270b44 100644 --- a/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx +++ b/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx @@ -47,14 +47,14 @@ export const TripleChartRow = (props: ChartRowProps) => ; TripleChartRow.defaultProps = { chartCount: 3, - chartHeight: 160, + chartHeight: 120, }; export const DoubleChartRow = (props: ChartRowProps) => ; DoubleChartRow.defaultProps = { chartCount: 2, - chartHeight: 300, + chartHeight: 220, }; const StyledRow = styled(PerformanceLayoutBodyRow)` diff --git a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx index fd949f80fa59f4..0b645619c6207c 100644 --- a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx +++ b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx @@ -10,6 +10,7 @@ import ContextMenu from 'app/views/dashboardsV2/contextMenu'; import {GenericPerformanceWidgetDataType} from '../types'; import {PerformanceWidgetSetting, WIDGET_DEFINITIONS} from '../widgetDefinitions'; +import {LineChartListWidget} from '../widgets/lineChartListWidget'; import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget'; import {ChartRowProps} from './widgetChartRow'; @@ -75,6 +76,7 @@ const _WidgetContainer = (props: Props) => { }; const widgetProps = { + chartSetting, ...WIDGET_DEFINITIONS({organization})[chartSetting], ContainerActions: containerProps => ( { throw new Error('Trends not currently supported.'); case GenericPerformanceWidgetDataType.area: return ; + case GenericPerformanceWidgetDataType.line_list: + return ; default: throw new Error(`Widget type "${widgetProps.dataType}" has no implementation.`); } diff --git a/static/app/views/performance/landing/widgets/transforms/transformDiscoverToList.tsx b/static/app/views/performance/landing/widgets/transforms/transformDiscoverToList.tsx new file mode 100644 index 00000000000000..737ff06e941e31 --- /dev/null +++ b/static/app/views/performance/landing/widgets/transforms/transformDiscoverToList.tsx @@ -0,0 +1,31 @@ +import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams'; +import {defined} from 'app/utils'; +import {TableData} from 'app/utils/discover/discoverQuery'; +import {GenericChildrenProps} from 'app/utils/discover/genericDiscoverQuery'; + +import {QueryDefinitionWithKey, WidgetDataConstraint, WidgetPropUnion} from '../types'; + +export function transformDiscoverToList( + widgetProps: WidgetPropUnion, + results: GenericChildrenProps, + _: QueryDefinitionWithKey +) { + const {start, end, utc, interval, statsPeriod} = getParams(widgetProps.location.query); + + const data = results.tableData?.data ?? []; + + const childData = { + ...results, + isErrored: !!results.error, + hasData: defined(data) && !!data.length, + data, + + utc: utc === 'true', + interval, + statsPeriod: statsPeriod ?? undefined, + start: start ?? '', + end: end ?? '', + }; + + return childData; +} diff --git a/static/app/views/performance/landing/widgets/transforms/transformEventsToArea.tsx b/static/app/views/performance/landing/widgets/transforms/transformEventsToArea.tsx index 07d493e3a7b735..0dfe6a0ac3d107 100644 --- a/static/app/views/performance/landing/widgets/transforms/transformEventsToArea.tsx +++ b/static/app/views/performance/landing/widgets/transforms/transformEventsToArea.tsx @@ -31,7 +31,7 @@ export function transformEventsRequestToArea( ...results, isLoading: results.loading, isErrored: results.errored, - hasData: defined(data) && data.length && !!data[0].data.length, + hasData: defined(data) && !!data.length && !!data[0].data.length, data, dataMean, previousData: results.previousTimeseriesData ?? undefined, diff --git a/static/app/views/performance/landing/widgets/types.tsx b/static/app/views/performance/landing/widgets/types.tsx index 381bc2c2d969df..42b38a5216b5d0 100644 --- a/static/app/views/performance/landing/widgets/types.tsx +++ b/static/app/views/performance/landing/widgets/types.tsx @@ -4,7 +4,7 @@ import {Location} from 'history'; import {Client} from 'app/api'; import BaseChart from 'app/components/charts/baseChart'; import {RenderProps} from 'app/components/charts/eventsRequest'; -import {DateString, Organization} from 'app/types'; +import {DateString, Organization, OrganizationSummary} from 'app/types'; import EventView from 'app/utils/discover/eventView'; import {PerformanceWidgetContainerTypes} from './components/performanceWidgetContainer'; @@ -20,6 +20,7 @@ export enum GenericPerformanceWidgetDataType { histogram = 'histogram', area = 'area', vitals = 'vitals', + line_list = 'line_list', trends = 'trends', } @@ -35,7 +36,7 @@ export interface WidgetDataConstraint { export type QueryChildren = { children: (props: any) => ReactNode; // TODO(k-fish): Fix any type. }; -export type QueryFC = FunctionComponent< +export type QueryFC = FunctionComponent< QueryChildren & { fields?: string | string[]; yAxis?: string | string[]; @@ -45,7 +46,12 @@ export type QueryFC = FunctionComponent< project?: Readonly; environment?: Readonly; team?: Readonly; - organization?: Organization; + query?: string; + orgSlug: string; + location: Location; + organization: OrganizationSummary; + eventView: EventView; + widgetData: T; } >; @@ -53,7 +59,7 @@ export type QueryDefinition< T extends WidgetDataConstraint, S extends WidgetDataResult | undefined > = { - component: QueryFC; + component: QueryFC; fields: string | string[]; enabled?: (data: T) => boolean; transform: ( diff --git a/static/app/views/performance/landing/widgets/widgetDefinitions.tsx b/static/app/views/performance/landing/widgets/widgetDefinitions.tsx index 43b592ac065743..45fe21e2a643d6 100644 --- a/static/app/views/performance/landing/widgets/widgetDefinitions.tsx +++ b/static/app/views/performance/landing/widgets/widgetDefinitions.tsx @@ -30,6 +30,8 @@ export enum PerformanceWidgetSetting { WORST_LCP_VITALS = 'worst_lcp_vitals', MOST_IMPROVED = 'most_improved', MOST_REGRESSED = 'most_regressed', + MOST_RELATED_ERRORS = 'most_related_errors', + MOST_RELATED_ISSUES = 'most_related_issues', } const WIDGET_PALETTE = CHART_PALETTE[5]; @@ -122,6 +124,20 @@ export const WIDGET_DEFINITIONS: ({ dataType: GenericPerformanceWidgetDataType.area, chartColor: WIDGET_PALETTE[0], }, + [PerformanceWidgetSetting.MOST_RELATED_ERRORS]: { + title: t('Most Related Errors'), + titleTooltip: getTermHelp(organization, PERFORMANCE_TERM.USER_MISERY), + fields: [`failure_count()`], + dataType: GenericPerformanceWidgetDataType.line_list, + chartColor: WIDGET_PALETTE[0], + }, + [PerformanceWidgetSetting.MOST_RELATED_ISSUES]: { + title: t('Most Related Issues'), + titleTooltip: getTermHelp(organization, PERFORMANCE_TERM.USER_MISERY), + fields: [`count()`], + dataType: GenericPerformanceWidgetDataType.line_list, + chartColor: WIDGET_PALETTE[0], + }, [PerformanceWidgetSetting.MOST_IMPROVED]: { title: t('Most Improved'), titleTooltip: t( diff --git a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx new file mode 100644 index 00000000000000..6f2d88e7a9d6be --- /dev/null +++ b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx @@ -0,0 +1,267 @@ +import {Fragment, FunctionComponent, ReactText, useMemo, useState} from 'react'; +import {browserHistory, withRouter} from 'react-router'; +import styled from '@emotion/styled'; +import {Location} from 'history'; + +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 {IconClose} from 'app/icons'; +import {t} from 'app/locale'; +import {Organization} from 'app/types'; +import DiscoverQuery from 'app/utils/discover/discoverQuery'; +import EventView from 'app/utils/discover/eventView'; +import {MutableSearch} from 'app/utils/tokenizeSearch'; +import withApi from 'app/utils/withApi'; +import _DurationChart from 'app/views/performance/charts/chart'; +import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils'; + +import {GenericPerformanceWidget} from '../components/performanceWidget'; +import SelectableList, {RightAlignedCell} from '../components/selectableList'; +import {transformDiscoverToList} from '../transforms/transformDiscoverToList'; +import {transformEventsRequestToArea} from '../transforms/transformEventsToArea'; +import {QueryDefinition, WidgetDataResult} from '../types'; +import {PerformanceWidgetSetting} from '../widgetDefinitions'; + +type Props = { + title: string; + titleTooltip: string; + fields: string[]; + chartColor?: string; + + eventView: EventView; + location: Location; + organization: Organization; + chartSetting: PerformanceWidgetSetting; + + ContainerActions: FunctionComponent<{isLoading: boolean}>; +}; + +type DataType = { + chart: WidgetDataResult & ReturnType; + 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; + + if (props.fields.length !== 1) { + throw new Error( + `Line chart list widget can only accept a single field (${props.fields})` + ); + } + + const Queries = { + list: useMemo>( + () => ({ + fields: props.fields[0], + component: provided => { + const eventView = provided.eventView.clone(); + eventView.sorts = [{kind: 'desc', field: props.fields[0]}]; + if (props.chartSetting === PerformanceWidgetSetting.MOST_RELATED_ISSUES) { + eventView.fields = [ + {field: 'issue'}, + {field: 'transaction'}, + {field: 'title'}, + {field: 'project.id'}, + {field: props.fields[0]}, + ]; + eventView.additionalConditions.setFilterValues('event.type', ['error']); + eventView.additionalConditions.setFilterValues('!tags[transaction]', ['']); + const mutableSearch = new MutableSearch(eventView.query); + mutableSearch.removeFilter('transaction.duration'); + eventView.query = mutableSearch.formatString(); + } else { + eventView.fields = [ + {field: 'transaction'}, + {field: 'project.id'}, + {field: props.fields[0]}, + ]; + } + return ; + }, + transform: transformDiscoverToList, + }), + [props.eventView, props.fields, props.chartSetting, props.organization.slug] + ), + chart: useMemo>( + () => ({ + enabled: widgetData => { + return !!widgetData?.list?.data?.length; + }, + fields: props.fields[0], + component: provided => { + const eventView = provided.eventView.clone(); + eventView.additionalConditions.setFilterValues('transaction', [ + provided.widgetData.list.data[selectedListIndex].transaction as string, + ]); + if (props.chartSetting === PerformanceWidgetSetting.MOST_RELATED_ISSUES) { + eventView.fields = [ + {field: 'issue'}, + {field: 'issue.id'}, + {field: 'transaction'}, + {field: props.fields[0]}, + ]; + eventView.additionalConditions.setFilterValues('issue', [ + provided.widgetData.list.data[selectedListIndex].issue as string, + ]); + eventView.additionalConditions.setFilterValues('event.type', ['error']); + eventView.additionalConditions.setFilterValues('!tags[transaction]', ['']); + const mutableSearch = new MutableSearch(eventView.query); + mutableSearch.removeFilter('transaction.duration'); + eventView.query = mutableSearch.formatString(); + } else { + eventView.fields = [{field: 'transaction'}, {field: props.fields[0]}]; + } + return ( + + ); + }, + transform: transformEventsRequestToArea, + }), + [ + props.eventView, + props.fields, + props.organization.slug, + props.chartSetting, + selectedListIndex, + ] + ), + }; + + return ( + + {...props} + subtitle={{t('Suggested transactions')}} + HeaderActions={provided => ( + + )} + Queries={Queries} + Visualizations={[ + { + component: provided => ( + + ), + height: 160, + }, + { + component: provided => ( + () => { + const transactionTarget = transactionSummaryRouteWithQuery({ + orgSlug: props.organization.slug, + projectID: listItem['project.id'] as string, + transaction: listItem.transaction as string, + query: props.eventView.getGlobalSelectionQuery(), + }); + switch (props.chartSetting) { + case PerformanceWidgetSetting.MOST_RELATED_ISSUES: + return ( + + {listItem.transaction} + + + + {listItem.issue} + + + + + + excludeTransaction(listItem.transaction, props) + } + /> + + + ); + default: + return ( + + {listItem.transaction} + {listItem.failure_count} + + + excludeTransaction(listItem.transaction, props) + } + /> + + + ); + } + })} + /> + ), + height: 200, + noPadding: true, + }, + ]} + /> + ); +} + +const EventsRequest = withApi(_EventsRequest); +const DurationChart = withRouter(_DurationChart); +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 StyledIconClose = styled(IconClose)` + cursor: pointer; + color: ${p => p.theme.gray200}; + + &:hover { + color: ${p => p.theme.gray300}; + } +`; diff --git a/tests/js/spec/views/performance/landing/index.spec.tsx b/tests/js/spec/views/performance/landing/index.spec.tsx index 9d053d257554fd..32e81a80fc1df9 100644 --- a/tests/js/spec/views/performance/landing/index.spec.tsx +++ b/tests/js/spec/views/performance/landing/index.spec.tsx @@ -27,6 +27,7 @@ const WrappedComponent = ({data}) => { describe('Performance > Landing > Index', function () { let eventStatsMock: any; + let eventsV2Mock: any; beforeEach(function () { // @ts-expect-error MockApiClient.addMockResponse({ @@ -62,6 +63,12 @@ describe('Performance > Landing > Index', function () { url: `/organizations/org-slug/events-trends-stats/`, body: [], }); + // @ts-expect-error + eventsV2Mock = MockApiClient.addMockResponse({ + method: 'GET', + url: `/organizations/org-slug/eventsv2/`, + body: [], + }); }); afterEach(function () { @@ -150,7 +157,8 @@ describe('Performance > Landing > Index', function () { expect(wrapper.find('Table').exists()).toBe(true); - expect(eventStatsMock).toHaveBeenCalledTimes(5); // Currently defaulting to 5 event stat charts on all transactions view. + expect(eventStatsMock).toHaveBeenCalledTimes(4); // Currently defaulting to 4 event stat charts on all transactions view + 1 event chart. + expect(eventsV2Mock).toHaveBeenCalledTimes(1); const titles = wrapper.find('div[data-test-id="performance-widget-title"]'); expect(titles).toHaveLength(5); @@ -159,6 +167,6 @@ describe('Performance > Landing > Index', function () { 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('Transactions Per Minute'); + expect(titles.at(4).text()).toEqual('Most Related Errors'); }); }); 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 a69cc976d858d4..9d13ccf77cfe80 100644 --- a/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx +++ b/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx @@ -24,12 +24,18 @@ const WrappedComponent = ({data, ...rest}) => { describe('Performance > Widgets > WidgetContainer', function () { let eventStatsMock; + let eventsV2Mock; beforeEach(function () { eventStatsMock = MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/events-stats/`, body: [], }); + eventsV2Mock = MockApiClient.addMockResponse({ + method: 'GET', + url: `/organizations/org-slug/eventsv2/`, + body: [], + }); }); it('TPM Widget', async function () { @@ -134,6 +140,74 @@ describe('Performance > Widgets > WidgetContainer', function () { ); }); + it('Most errors 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 Related Errors' + ); + expect(eventsV2Mock).toHaveBeenCalledTimes(1); + expect(eventsV2Mock).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ + query: expect.objectContaining({ + environment: [], + field: ['transaction', 'project.id', 'failure_count()'], + per_page: 3, + project: [], + query: '', + sort: '-failure_count()', + statsPeriod: '14d', + }), + }) + ); + }); + + it('Most related issues 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 Related Issues' + ); + expect(eventsV2Mock).toHaveBeenCalledTimes(1); + expect(eventsV2Mock).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ + query: expect.objectContaining({ + environment: [], + field: ['issue', 'transaction', 'title', 'project.id', 'count()'], + per_page: 3, + project: [], + query: 'event.type:error !tags[transaction]:""', + sort: '-count()', + statsPeriod: '14d', + }), + }) + ); + }); + it('Able to change widget type from menu', async function () { const data = initializeData();