import {Fragment} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import moment from 'moment-timezone'; import type {Client} from 'sentry/api'; import {Alert} from 'sentry/components/alert'; import {getInterval} from 'sentry/components/charts/utils'; import * as Layout from 'sentry/components/layouts/thirds'; import Link from 'sentry/components/links/link'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import Placeholder from 'sentry/components/placeholder'; import type {ChangeData} from 'sentry/components/timeRangeSelector'; import {TimeRangeSelector} from 'sentry/components/timeRangeSelector'; import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {RuleActionsCategories} from 'sentry/types/alerts'; import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {formatMRIField} from 'sentry/utils/metrics/mri'; import {shouldShowOnDemandMetricAlertUI} from 'sentry/utils/onDemandMetrics/features'; import AnomalyDetectionFeedbackBanner from 'sentry/views/alerts/rules/metric/details/anomalyDetectionFeedbackBanner'; import {ErrorMigrationWarning} from 'sentry/views/alerts/rules/metric/details/errorMigrationWarning'; import MetricHistory from 'sentry/views/alerts/rules/metric/details/metricHistory'; import type {MetricRule} from 'sentry/views/alerts/rules/metric/types'; import { AlertRuleComparisonType, Dataset, TimePeriod, } from 'sentry/views/alerts/rules/metric/types'; import {extractEventTypeFilterFromRule} from 'sentry/views/alerts/rules/metric/utils/getEventTypeFilter'; import {isInsightsMetricAlert} from 'sentry/views/alerts/rules/metric/utils/isInsightsMetricAlert'; import {isOnDemandMetricAlert} from 'sentry/views/alerts/rules/metric/utils/onDemandMetricAlert'; import {getAlertRuleActionCategory} from 'sentry/views/alerts/rules/utils'; import type {Anomaly, Incident} from 'sentry/views/alerts/types'; import {AlertRuleStatus} from 'sentry/views/alerts/types'; import {alertDetailsLink} from 'sentry/views/alerts/utils'; import {MetricsBetaEndAlert} from 'sentry/views/metrics/metricsBetaEndAlert'; import {isCrashFreeAlert} from '../utils/isCrashFreeAlert'; import {isCustomMetricAlert} from '../utils/isCustomMetricAlert'; import type {TimePeriodType} from './constants'; import { API_INTERVAL_POINTS_LIMIT, SELECTOR_RELATIVE_PERIODS, TIME_WINDOWS, } from './constants'; import MetricChart from './metricChart'; import RelatedIssues from './relatedIssues'; import RelatedTransactions from './relatedTransactions'; import {MetricDetailsSidebar} from './sidebar'; interface MetricDetailsBodyProps extends RouteComponentProps<{}, {}> { api: Client; location: Location; organization: Organization; timePeriod: TimePeriodType; anomalies?: Anomaly[]; incidents?: Incident[]; project?: Project; rule?: MetricRule; selectedIncident?: Incident | null; } export default function MetricDetailsBody({ api, project, rule, incidents, organization, timePeriod, selectedIncident, location, router, anomalies, }: MetricDetailsBodyProps) { function getPeriodInterval() { const startDate = moment.utc(timePeriod.start); const endDate = moment.utc(timePeriod.end); const timeWindow = rule?.timeWindow; const startEndDifferenceMs = endDate.diff(startDate); if ( timeWindow && (startEndDifferenceMs < API_INTERVAL_POINTS_LIMIT * timeWindow * 60 * 1000 || // Special case 7 days * 1m interval over the api limit startEndDifferenceMs === TIME_WINDOWS[TimePeriod.SEVEN_DAYS]) ) { return `${timeWindow}m`; } return getInterval({start: timePeriod.start, end: timePeriod.end}, 'high'); } function getFilter(): string[] | null { if (!rule) { return null; } const {aggregate, dataset, query} = rule; if (isCrashFreeAlert(dataset) || isCustomMetricAlert(aggregate)) { return query.trim().split(' '); } const eventType = extractEventTypeFilterFromRule(rule); return (query ? `(${eventType}) AND (${query.trim()})` : eventType).split(' '); } const handleTimePeriodChange = (datetime: ChangeData) => { const {start, end, relative} = datetime; if (start && end) { return router.push({ ...location, query: { start: moment(start).utc().format(), end: moment(end).utc().format(), }, }); } return router.push({ ...location, query: { period: relative, }, }); }; if (!rule || !project) { return ( ); } const {dataset, aggregate, query} = rule; const eventType = extractEventTypeFilterFromRule(rule); const queryWithTypeFilter = dataset === Dataset.EVENTS_ANALYTICS_PLATFORM ? query : (query ? `(${query}) AND (${eventType})` : eventType).trim(); const relativeOptions = { ...SELECTOR_RELATIVE_PERIODS, ...(rule.timeWindow > 1 ? {[TimePeriod.FOURTEEN_DAYS]: t('Last 14 days')} : {}), ...(rule.detectionType === AlertRuleComparisonType.DYNAMIC ? {[TimePeriod.TWENTY_EIGHT_DAYS]: t('Last 28 days')} : {}), }; const isSnoozed = rule.snooze; const ruleActionCategory = getAlertRuleActionCategory(rule); const showOnDemandMetricAlertUI = isOnDemandMetricAlert(dataset, aggregate, query) && shouldShowOnDemandMetricAlertUI(organization); let formattedAggregate = aggregate; if (isCustomMetricAlert(aggregate)) { formattedAggregate = formatMRIField(aggregate); } return ( {isCustomMetricAlert(rule.aggregate) && !isInsightsMetricAlert(rule.aggregate) && ( )} {selectedIncident?.alertRule.status === AlertRuleStatus.SNAPSHOT && ( {t('Alert Rule settings have been updated since this alert was triggered.')} )} {isSnoozed && ( {ruleActionCategory === RuleActionsCategories.NO_DEFAULT ? tct( "[creator] muted this alert so these notifications won't be sent in the future.", {creator: rule.snoozeCreatedBy} ) : tct( "[creator] muted this alert[forEveryone]so you won't get these notifications in the future.", { creator: rule.snoozeCreatedBy, forEveryone: rule.snoozeForEveryone ? ' for everyone ' : ' ', } )} )} {selectedIncident && ( Remove filter on alert #{selectedIncident.identifier} )} {selectedIncident?.alertRule.detectionType === AlertRuleComparisonType.DYNAMIC && ( )} {/* TODO: add activation start/stop into chart */} {[Dataset.METRICS, Dataset.SESSIONS, Dataset.ERRORS].includes(dataset) && ( )} {dataset === Dataset.TRANSACTIONS && ( )} ); } const DetailWrapper = styled('div')` display: flex; flex: 1; @media (max-width: ${p => p.theme.breakpoints.small}) { flex-direction: column-reverse; } `; const StyledLayoutBody = styled(Layout.Body)` flex-grow: 0; padding-bottom: 0 !important; @media (min-width: ${p => p.theme.breakpoints.medium}) { grid-template-columns: auto; } `; const StyledAlert = styled(Alert)` margin: 0; `; const ActivityWrapper = styled('div')` display: flex; flex: 1; flex-direction: column; width: 100%; `; const ChartPanel = styled(Panel)` margin-top: ${space(2)}; `; const StyledSubHeader = styled('div')` margin-bottom: ${space(2)}; display: flex; align-items: center; `; const StyledTimeRangeSelector = styled(TimeRangeSelector)` margin-right: ${space(1)}; `;