mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
288 lines
9.2 KiB
TypeScript
288 lines
9.2 KiB
TypeScript
import {Fragment} from 'react';
|
|
import type {RouteComponentProps} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import type {Location} from 'history';
|
|
import moment from 'moment';
|
|
|
|
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 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 {t, tct} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization, Project} from 'sentry/types';
|
|
import {RuleActionsCategories} from 'sentry/types/alerts';
|
|
import {shouldShowOnDemandMetricAlertUI} from 'sentry/utils/onDemandMetrics/features';
|
|
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 {Dataset, TimePeriod} from 'sentry/views/alerts/rules/metric/types';
|
|
import {extractEventTypeFilterFromRule} from 'sentry/views/alerts/rules/metric/utils/getEventTypeFilter';
|
|
import {isOnDemandMetricAlert} from 'sentry/views/alerts/rules/metric/utils/onDemandMetricAlert';
|
|
import {getAlertRuleActionCategory} from 'sentry/views/alerts/rules/utils';
|
|
import type {Incident} from 'sentry/views/alerts/types';
|
|
import {AlertRuleStatus} from 'sentry/views/alerts/types';
|
|
|
|
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;
|
|
incidents?: Incident[];
|
|
project?: Project;
|
|
rule?: MetricRule;
|
|
selectedIncident?: Incident | null;
|
|
}
|
|
|
|
export default function MetricDetailsBody({
|
|
api,
|
|
project,
|
|
rule,
|
|
incidents,
|
|
organization,
|
|
timePeriod,
|
|
selectedIncident,
|
|
location,
|
|
router,
|
|
}: 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 (
|
|
<Layout.Body>
|
|
<Layout.Main>
|
|
<Placeholder height="38px" />
|
|
<ChartPanel>
|
|
<PanelBody withPadding>
|
|
<Placeholder height="200px" />
|
|
</PanelBody>
|
|
</ChartPanel>
|
|
</Layout.Main>
|
|
<Layout.Side>
|
|
<Placeholder height="200px" />
|
|
</Layout.Side>
|
|
</Layout.Body>
|
|
);
|
|
}
|
|
|
|
const {dataset, aggregate, query} = rule;
|
|
|
|
const eventType = extractEventTypeFilterFromRule(rule);
|
|
const queryWithTypeFilter = (
|
|
query ? `(${query}) AND (${eventType})` : eventType
|
|
).trim();
|
|
const relativeOptions = {
|
|
...SELECTOR_RELATIVE_PERIODS,
|
|
...(rule.timeWindow > 1 ? {[TimePeriod.FOURTEEN_DAYS]: t('Last 14 days')} : {}),
|
|
};
|
|
|
|
const isSnoozed = rule.snooze;
|
|
const ruleActionCategory = getAlertRuleActionCategory(rule);
|
|
|
|
const showOnDemandMetricAlertUI =
|
|
isOnDemandMetricAlert(dataset, aggregate, query) &&
|
|
shouldShowOnDemandMetricAlertUI(organization);
|
|
|
|
return (
|
|
<Fragment>
|
|
{selectedIncident?.alertRule.status === AlertRuleStatus.SNAPSHOT && (
|
|
<StyledLayoutBody>
|
|
<StyledAlert type="warning" showIcon>
|
|
{t('Alert Rule settings have been updated since this alert was triggered.')}
|
|
</StyledAlert>
|
|
</StyledLayoutBody>
|
|
)}
|
|
<Layout.Body>
|
|
<Layout.Main>
|
|
{isSnoozed && (
|
|
<Alert showIcon>
|
|
{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 ' : ' ',
|
|
}
|
|
)}
|
|
</Alert>
|
|
)}
|
|
<StyledTimeRangeSelector
|
|
relative={timePeriod.period ?? ''}
|
|
start={(timePeriod.custom && timePeriod.start) || null}
|
|
end={(timePeriod.custom && timePeriod.end) || null}
|
|
onChange={handleTimePeriodChange}
|
|
relativeOptions={relativeOptions}
|
|
showAbsolute={false}
|
|
disallowArbitraryRelativeRanges
|
|
triggerLabel={relativeOptions[timePeriod.period ?? '']}
|
|
/>
|
|
|
|
<ErrorMigrationWarning project={project} rule={rule} />
|
|
|
|
<MetricChart
|
|
api={api}
|
|
rule={rule}
|
|
incidents={incidents}
|
|
timePeriod={timePeriod}
|
|
selectedIncident={selectedIncident}
|
|
organization={organization}
|
|
project={project}
|
|
interval={getPeriodInterval()}
|
|
query={isCrashFreeAlert(dataset) ? query : queryWithTypeFilter}
|
|
filter={getFilter()}
|
|
isOnDemandAlert={isOnDemandMetricAlert(dataset, aggregate, query)}
|
|
/>
|
|
<DetailWrapper>
|
|
<ActivityWrapper>
|
|
<MetricHistory incidents={incidents} />
|
|
{[Dataset.METRICS, Dataset.SESSIONS, Dataset.ERRORS].includes(dataset) && (
|
|
<RelatedIssues
|
|
organization={organization}
|
|
rule={rule}
|
|
projects={[project]}
|
|
timePeriod={timePeriod}
|
|
query={
|
|
dataset === Dataset.ERRORS
|
|
? // Not using (query) AND (event.type:x) because issues doesn't support it yet
|
|
`${extractEventTypeFilterFromRule(rule)} ${query}`.trim()
|
|
: isCrashFreeAlert(dataset)
|
|
? `${query} error.unhandled:true`.trim()
|
|
: undefined
|
|
}
|
|
/>
|
|
)}
|
|
{dataset === Dataset.TRANSACTIONS && (
|
|
<RelatedTransactions
|
|
organization={organization}
|
|
location={location}
|
|
rule={rule}
|
|
projects={[project]}
|
|
timePeriod={timePeriod}
|
|
filter={extractEventTypeFilterFromRule(rule)}
|
|
/>
|
|
)}
|
|
</ActivityWrapper>
|
|
</DetailWrapper>
|
|
</Layout.Main>
|
|
<Layout.Side>
|
|
<MetricDetailsSidebar
|
|
rule={rule}
|
|
showOnDemandMetricAlertUI={showOnDemandMetricAlertUI}
|
|
/>
|
|
</Layout.Side>
|
|
</Layout.Body>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
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 StyledTimeRangeSelector = styled(TimeRangeSelector)`
|
|
margin-bottom: ${space(2)};
|
|
`;
|