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; 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; type DataType = { chart: AnomalyData; }; function Anomalies(props: AnomaliesSectionProps) { const height = 250; const chartColor = theme.charts.colors[0]; const chart = useMemo>(() => { return { fields: '', component: provided => {provided.children(props.queryData)}, transform: transformAnomalyData, }; }, [props.queryData]); return ( {...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={() =>
} HeaderActions={() =>
} 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 ( ); }, 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 ( {queryData => ( )} ); } 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;