Files
2024-02-15 17:01:50 +00:00

330 lines
9.1 KiB
TypeScript

import {Fragment, useMemo} from 'react';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';
import type {Location} from 'history';
import omit from 'lodash/omit';
import MarkArea from 'sentry/components/charts/components/markArea';
import MarkLine from 'sentry/components/charts/components/markLine';
import type {LineChartSeries} from 'sentry/components/charts/lineChart';
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 {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types';
import {defined} from 'sentry/utils';
import type EventView from 'sentry/utils/discover/eventView';
import type {
AnomalyInfo,
AnomalyPayload,
ChildrenProps,
} from 'sentry/utils/performance/anomalies/anomaliesQuery';
import AnomaliesQuery from 'sentry/utils/performance/anomalies/anomaliesQuery';
import {decodeScalar} from 'sentry/utils/queryString';
import theme from 'sentry/utils/theme';
import {GenericPerformanceWidget} from '../../landing/widgets/components/performanceWidget';
import {WidgetEmptyStateWarning} from '../../landing/widgets/components/selectableList';
import type {QueryDefinition, WidgetDataResult} from '../../landing/widgets/types';
import {
PerformanceWidgetSetting,
WIDGET_DEFINITIONS,
} from '../../landing/widgets/widgetDefinitions';
import type {SetStateAction} from '../types';
import AnomaliesTable from './anomaliesTable';
import {AnomalyChart} from './anomalyChart';
type Props = {
eventView: EventView;
location: Location;
organization: Organization;
projectId: string;
setError: SetStateAction<string | undefined>;
transactionName: string;
};
type AnomaliesSectionProps = Props & {
queryData: ChildrenProps;
};
const anomalyAreaName = (anomaly: AnomalyInfo) => `#${anomaly.id}`;
const transformAnomalyToArea = (
anomaly: AnomalyInfo
): [{name: string; xAxis: number}, {xAxis: number}] => [
{name: anomalyAreaName(anomaly), xAxis: anomaly.start},
{xAxis: anomaly.end},
];
const transformAnomalyData = (
_: any,
results: {data: AnomalyPayload; error: null | string; isLoading: boolean}
) => {
const data: LineChartSeries[] = [];
const resultData = results.data;
if (!resultData) {
return {
isLoading: results.isLoading,
isErrored: !!results.error,
data: undefined,
hasData: false,
loading: results.isLoading,
};
}
data.push({
seriesName: 'tpm()',
data: resultData.y.data.map(([name, [{count}]]) => ({
name,
value: count,
})),
});
data.push({
seriesName: 'tpm() lower bound',
data: resultData.yhat_lower.data.map(([name, [{count}]]) => ({
name,
value: count,
})),
});
data.push({
seriesName: 'tpm() upper bound',
data: resultData.yhat_upper.data.map(([name, [{count}]]) => ({
name,
value: count,
})),
});
const anomalies = results.data.anomalies;
const highConfidenceAreas = anomalies
.filter(a => a.confidence === 'high')
.map(transformAnomalyToArea);
const highConfidenceLines = anomalies
.filter(a => a.confidence === 'high')
.map(area => ({xAxis: area.start, name: anomalyAreaName(area)}));
const lowConfidenceAreas = anomalies
.filter(a => a.confidence === 'low')
.map(transformAnomalyToArea);
const lowConfidenceLines = anomalies
.filter(a => a.confidence === 'low')
.map(area => ({xAxis: area.start, name: anomalyAreaName(area)}));
data.push({
seriesName: 'High Confidence',
color: theme.red300,
data: [],
silent: true,
markLine: MarkLine({
animation: false,
lineStyle: {color: theme.red300, type: 'solid', width: 1, opacity: 1.0},
data: highConfidenceLines,
label: {
show: true,
rotate: 90,
color: theme.red300,
position: 'insideEndBottom',
fontSize: '10',
offset: [5, 5],
formatter: obj => `${(obj.data as any).name}`,
},
}),
markArea: MarkArea({
itemStyle: {
color: theme.red300,
opacity: 0.2,
},
label: {
show: false,
},
data: highConfidenceAreas,
}),
});
data.push({
seriesName: 'Low Confidence',
color: theme.yellow200,
data: [],
markLine: MarkLine({
animation: false,
lineStyle: {color: theme.yellow200, type: 'solid', width: 1, opacity: 1.0},
data: lowConfidenceLines,
label: {
show: true,
rotate: 90,
color: theme.yellow300,
position: 'insideEndBottom',
fontSize: '10',
offset: [5, 5],
formatter: obj => `${(obj.data as any).name}`,
},
}),
markArea: MarkArea({
itemStyle: {
color: theme.yellow200,
opacity: 0.2,
},
label: {
show: false,
},
data: lowConfidenceAreas,
}),
});
return {
isLoading: results.isLoading,
isErrored: !!results.error,
data,
hasData: true,
loading: results.isLoading,
};
};
type AnomalyData = WidgetDataResult & ReturnType<typeof transformAnomalyData>;
type DataType = {
chart: AnomalyData;
};
function Anomalies(props: AnomaliesSectionProps) {
const height = 250;
const chartColor = theme.charts.colors[0];
const chart = useMemo<QueryDefinition<DataType, WidgetDataResult>>(() => {
return {
fields: '',
component: provided => <Fragment>{provided.children(props.queryData)}</Fragment>,
transform: transformAnomalyData,
};
}, [props.queryData]);
return (
<GenericPerformanceWidget<DataType>
{...props}
title={t('Transaction Count')}
titleTooltip={t(
'Represents transaction count across time, with added visualizations to highlight anomalies in your data.'
)}
fields={['']}
chartSetting={PerformanceWidgetSetting.TPM_AREA}
chartDefinition={WIDGET_DEFINITIONS[PerformanceWidgetSetting.TPM_AREA]}
Subtitle={() => <div />}
HeaderActions={() => <div />}
EmptyComponent={WidgetEmptyStateWarning}
Queries={{
chart,
}}
Visualizations={[
{
component: provided => {
const data =
provided.widgetData.chart.data?.map(series => {
if (series.seriesName !== 'tpm()') {
series.lineStyle = {type: 'dashed', color: chartColor, width: 1.5};
}
if (series.seriesName === 'score') {
series.lineStyle = {color: theme.red400};
}
return series;
}) ?? [];
return (
<AnomalyChart
{...provided}
data={data}
height={height}
statsPeriod={undefined}
start={null}
end={null}
/>
);
},
height,
},
]}
/>
);
}
function AnomaliesContent(props: Props) {
const {location, organization, eventView} = props;
const query = decodeScalar(location.query.query, '');
function handleChange(key: string) {
return function (value: string | undefined) {
const queryParams = normalizeDateTimeParams({
...(location.query || {}),
[key]: value,
});
// do not propagate pagination when making a new search
const toOmit = ['cursor'];
if (!defined(value)) {
toOmit.push(key);
}
const searchQueryParams = omit(queryParams, toOmit);
browserHistory.push({
...location,
query: searchQueryParams,
});
};
}
return (
<Layout.Main fullWidth>
<FilterActions>
<PageFilterBar condensed>
<EnvironmentPageFilter />
<DatePageFilter />
</PageFilterBar>
<SearchBar
organization={organization}
projectIds={eventView.project}
query={query}
fields={eventView.fields}
onSearch={handleChange('query')}
/>
</FilterActions>
<AnomaliesQuery
organization={organization}
location={location}
eventView={eventView}
>
{queryData => (
<Fragment>
<AnomaliesWrapper>
<Anomalies {...props} queryData={queryData} />
</AnomaliesWrapper>
<AnomaliesTable
anomalies={queryData.data?.anomalies}
{...props}
isLoading={queryData.isLoading}
/>
</Fragment>
)}
</AnomaliesQuery>
</Layout.Main>
);
}
const FilterActions = styled('div')`
display: grid;
gap: ${space(2)};
margin-bottom: ${space(2)};
@media (min-width: ${p => p.theme.breakpoints.small}) {
grid-template-columns: auto 1fr;
}
`;
const AnomaliesWrapper = styled('div')`
margin-bottom: ${space(2)};
`;
export default AnomaliesContent;