mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
277 lines
7.6 KiB
TypeScript
277 lines
7.6 KiB
TypeScript
import {t} from 'sentry/locale';
|
|
import {parsePeriodToHours} from 'sentry/utils/dates';
|
|
import type EventView from 'sentry/utils/discover/eventView';
|
|
import type {AggregationKeyWithAlias, LooseFieldKey} from 'sentry/utils/discover/fields';
|
|
import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/discover/fields';
|
|
import {AggregationKey} from 'sentry/utils/fields';
|
|
import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
|
|
import type {Trigger, UnsavedMetricRule} from 'sentry/views/alerts/rules/metric/types';
|
|
import {
|
|
AlertRuleComparisonType,
|
|
AlertRuleThresholdType,
|
|
AlertRuleTriggerType,
|
|
Dataset,
|
|
Datasource,
|
|
EventTypes,
|
|
TimeWindow,
|
|
} from 'sentry/views/alerts/rules/metric/types';
|
|
import {
|
|
DATA_SOURCE_TO_SET_AND_EVENT_TYPES,
|
|
getQueryDatasource,
|
|
isSessionAggregate,
|
|
} from 'sentry/views/alerts/utils';
|
|
import type {AlertType, WizardRuleTemplate} from 'sentry/views/alerts/wizard/options';
|
|
|
|
export const DEFAULT_COUNT_TIME_WINDOW = 1; // 1min
|
|
export const DEFAULT_CHANGE_TIME_WINDOW = 60; // 1h
|
|
export const DEFAULT_CHANGE_COMP_DELTA = 10080; // 1w
|
|
|
|
export const DEFAULT_AGGREGATE = 'count()';
|
|
export const DEFAULT_TRANSACTION_AGGREGATE = 'p95(transaction.duration)';
|
|
|
|
export const DATASET_EVENT_TYPE_FILTERS = {
|
|
[Dataset.ERRORS]: 'event.type:error',
|
|
[Dataset.TRANSACTIONS]: 'event.type:transaction',
|
|
[Dataset.GENERIC_METRICS]: 'event.type:transaction',
|
|
} as const;
|
|
|
|
export const DATASOURCE_EVENT_TYPE_FILTERS = {
|
|
[Datasource.ERROR_DEFAULT]: 'event.type:[error, default]',
|
|
[Datasource.ERROR]: 'event.type:error',
|
|
[Datasource.DEFAULT]: 'event.type:default',
|
|
[Datasource.TRANSACTION]: 'event.type:transaction',
|
|
} as const;
|
|
|
|
export type OptionConfig = {
|
|
aggregations: AggregationKeyWithAlias[];
|
|
fields: LooseFieldKey[];
|
|
measurementKeys?: string[];
|
|
spanOperationBreakdownKeys?: string[];
|
|
};
|
|
|
|
/**
|
|
* Allowed error aggregations for alerts
|
|
*/
|
|
export const errorFieldConfig: OptionConfig = {
|
|
aggregations: [AggregationKey.COUNT, AggregationKey.COUNT_UNIQUE],
|
|
fields: ['user'],
|
|
};
|
|
|
|
const commonAggregations = [
|
|
AggregationKey.AVG,
|
|
AggregationKey.P50,
|
|
AggregationKey.P75,
|
|
AggregationKey.P90,
|
|
AggregationKey.P95,
|
|
AggregationKey.P99,
|
|
AggregationKey.P100,
|
|
];
|
|
|
|
const allAggregations = [
|
|
...commonAggregations,
|
|
AggregationKey.FAILURE_RATE,
|
|
AggregationKey.APDEX,
|
|
AggregationKey.COUNT,
|
|
];
|
|
|
|
export const DuplicateMetricFields: string[] = [
|
|
'dataset',
|
|
'eventTypes',
|
|
'aggregate',
|
|
'query',
|
|
'timeWindow',
|
|
'thresholdPeriod',
|
|
'projects',
|
|
'environment',
|
|
'resolveThreshold',
|
|
'thresholdType',
|
|
'owner',
|
|
'name',
|
|
'projectId',
|
|
'comparisonDelta',
|
|
];
|
|
|
|
export const DuplicateTriggerFields: string[] = ['alertThreshold', 'label'];
|
|
|
|
export const DuplicateActionFields: string[] = [
|
|
'type',
|
|
'targetType',
|
|
'targetIdentifier',
|
|
'inputChannelId',
|
|
'options',
|
|
];
|
|
|
|
export const COMPARISON_DELTA_OPTIONS = [
|
|
{value: 5, label: t('same time 5 minutes ago')}, // 5 minutes
|
|
{value: 15, label: t('same time 15 minutes ago')}, // 15 minutes
|
|
{value: 60, label: t('same time one hour ago')}, // one hour
|
|
{value: 1440, label: t('same time one day ago')}, // one day
|
|
{value: 10080, label: t('same time one week ago')}, // one week
|
|
{value: 43200, label: t('same time one month ago')}, // 30 days
|
|
];
|
|
|
|
export function getWizardAlertFieldConfig(
|
|
alertType: AlertType,
|
|
dataset: Dataset
|
|
): OptionConfig {
|
|
if (alertType === 'custom_transactions' && dataset === Dataset.ERRORS) {
|
|
return errorFieldConfig;
|
|
}
|
|
// If user selected apdex we must include that in the OptionConfig as it has a user specified column
|
|
const aggregations =
|
|
alertType === 'apdex' || alertType === 'custom_transactions'
|
|
? allAggregations
|
|
: commonAggregations;
|
|
|
|
const config: OptionConfig = {
|
|
aggregations,
|
|
fields: ['transaction.duration'],
|
|
measurementKeys: Object.keys(WEB_VITAL_DETAILS),
|
|
};
|
|
|
|
if ([Dataset.TRANSACTIONS, Dataset.GENERIC_METRICS].includes(dataset)) {
|
|
config.spanOperationBreakdownKeys = SPAN_OP_BREAKDOWN_FIELDS;
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Allowed transaction aggregations for alerts
|
|
*/
|
|
export const transactionFieldConfig: OptionConfig = {
|
|
aggregations: allAggregations,
|
|
fields: ['transaction.duration'],
|
|
spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
|
|
measurementKeys: Object.keys(WEB_VITAL_DETAILS),
|
|
};
|
|
|
|
export function createDefaultTrigger(label: AlertRuleTriggerType): Trigger {
|
|
return {
|
|
label,
|
|
alertThreshold: '',
|
|
actions: [],
|
|
};
|
|
}
|
|
|
|
export function createDefaultRule(
|
|
defaultRuleOptions: Partial<UnsavedMetricRule> = {}
|
|
): UnsavedMetricRule {
|
|
return {
|
|
dataset: Dataset.ERRORS,
|
|
eventTypes: [EventTypes.ERROR],
|
|
aggregate: DEFAULT_AGGREGATE,
|
|
query: '',
|
|
timeWindow: 60,
|
|
thresholdPeriod: 1,
|
|
triggers: [
|
|
createDefaultTrigger(AlertRuleTriggerType.CRITICAL),
|
|
createDefaultTrigger(AlertRuleTriggerType.WARNING),
|
|
],
|
|
projects: [],
|
|
environment: null,
|
|
resolveThreshold: '',
|
|
thresholdType: AlertRuleThresholdType.ABOVE,
|
|
...defaultRuleOptions,
|
|
};
|
|
}
|
|
|
|
function getAlertTimeWindow(period: string | undefined): TimeWindow | undefined {
|
|
if (!period) {
|
|
return undefined;
|
|
}
|
|
|
|
const periodMinutes = parsePeriodToHours(period) * 60;
|
|
if (periodMinutes < 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const timeWindows = Object.values(TimeWindow)
|
|
.filter((value): value is TimeWindow => typeof value === 'number')
|
|
.sort((a, b) => a - b);
|
|
|
|
for (let index = 0; index < timeWindows.length; index++) {
|
|
const timeWindow = timeWindows[index];
|
|
if (periodMinutes <= timeWindow) {
|
|
return timeWindow;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Create an unsaved alert from a discover EventView object
|
|
*/
|
|
export function createRuleFromEventView(eventView: EventView): UnsavedMetricRule {
|
|
const parsedQuery = getQueryDatasource(eventView.query);
|
|
const datasetAndEventtypes = parsedQuery
|
|
? DATA_SOURCE_TO_SET_AND_EVENT_TYPES[parsedQuery.source]
|
|
: DATA_SOURCE_TO_SET_AND_EVENT_TYPES.error;
|
|
|
|
let aggregate = eventView.getYAxis();
|
|
if (
|
|
datasetAndEventtypes.dataset === 'transactions' &&
|
|
/^p\d{2,3}\(\)/.test(eventView.getYAxis())
|
|
) {
|
|
// p95() -> p95(transaction.duration)
|
|
aggregate = eventView.getYAxis().slice(0, 3) + '(transaction.duration)';
|
|
}
|
|
const defaultRule = createDefaultRule();
|
|
return {
|
|
...defaultRule,
|
|
...datasetAndEventtypes,
|
|
query: parsedQuery?.query ?? eventView.query,
|
|
aggregate,
|
|
timeWindow: getAlertTimeWindow(eventView.interval) ?? defaultRule.timeWindow,
|
|
environment: eventView.environment.length ? eventView.environment[0] : null,
|
|
};
|
|
}
|
|
|
|
export function createRuleFromWizardTemplate(
|
|
wizardTemplate: WizardRuleTemplate
|
|
): UnsavedMetricRule {
|
|
const {eventTypes, aggregate, dataset, query} = wizardTemplate;
|
|
const defaultRuleOptions: Partial<UnsavedMetricRule> = {};
|
|
|
|
if (isSessionAggregate(aggregate)) {
|
|
defaultRuleOptions.thresholdType = AlertRuleThresholdType.BELOW;
|
|
defaultRuleOptions.timeWindow = TimeWindow.ONE_HOUR;
|
|
}
|
|
|
|
if (aggregate.includes('apdex')) {
|
|
defaultRuleOptions.thresholdType = AlertRuleThresholdType.BELOW;
|
|
}
|
|
|
|
return {
|
|
...createDefaultRule(defaultRuleOptions),
|
|
eventTypes: [eventTypes],
|
|
aggregate,
|
|
dataset,
|
|
query: query ?? '',
|
|
};
|
|
}
|
|
|
|
export function getThresholdUnits(
|
|
aggregate: string,
|
|
comparisonType: AlertRuleComparisonType
|
|
): string {
|
|
// cls is a number not a measurement of time
|
|
if (
|
|
isSessionAggregate(aggregate) ||
|
|
comparisonType === AlertRuleComparisonType.CHANGE
|
|
) {
|
|
return '%';
|
|
}
|
|
|
|
if (aggregate.includes('measurements.cls')) {
|
|
return '';
|
|
}
|
|
|
|
if (aggregate.includes('duration') || aggregate.includes('measurements')) {
|
|
return 'ms';
|
|
}
|
|
|
|
return '';
|
|
}
|