mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
330 lines
9.1 KiB
TypeScript
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;
|