123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- import round from 'lodash/round';
- import {Client} from 'app/api';
- import {t} from 'app/locale';
- import {NewQuery, Project, SessionField} from 'app/types';
- import {IssueAlertRule} from 'app/types/alerts';
- import {defined} from 'app/utils';
- import {getUtcDateString} from 'app/utils/dates';
- import {axisLabelFormatter, tooltipFormatter} from 'app/utils/discover/charts';
- import EventView from 'app/utils/discover/eventView';
- import {getAggregateAlias} from 'app/utils/discover/fields';
- import {PRESET_AGGREGATES} from 'app/views/alerts/incidentRules/presets';
- import {
- Dataset,
- Datasource,
- EventTypes,
- IncidentRule,
- SavedIncidentRule,
- SessionsAggregate,
- } from 'app/views/alerts/incidentRules/types';
- import {Incident, IncidentStats, IncidentStatus} from '../types';
- // Use this api for requests that are getting cancelled
- const uncancellableApi = new Client();
- export function fetchAlertRule(orgId: string, ruleId: string): Promise<IncidentRule> {
- return uncancellableApi.requestPromise(
- `/organizations/${orgId}/alert-rules/${ruleId}/`
- );
- }
- export function fetchIncidentsForRule(
- orgId: string,
- alertRule: string,
- start: string,
- end: string
- ): Promise<Incident[]> {
- return uncancellableApi.requestPromise(`/organizations/${orgId}/incidents/`, {
- query: {
- alertRule,
- includeSnapshots: true,
- start,
- end,
- expand: ['activities', 'seen_by', 'original_alert_rule'],
- },
- });
- }
- export function fetchIncident(
- api: Client,
- orgId: string,
- alertId: string
- ): Promise<Incident> {
- return api.requestPromise(`/organizations/${orgId}/incidents/${alertId}/`);
- }
- export function fetchIncidentStats(
- api: Client,
- orgId: string,
- alertId: string
- ): Promise<IncidentStats> {
- return api.requestPromise(`/organizations/${orgId}/incidents/${alertId}/stats/`);
- }
- export function updateSubscription(
- api: Client,
- orgId: string,
- alertId: string,
- isSubscribed?: boolean
- ): Promise<Incident> {
- const method = isSubscribed ? 'POST' : 'DELETE';
- return api.requestPromise(
- `/organizations/${orgId}/incidents/${alertId}/subscriptions/`,
- {
- method,
- }
- );
- }
- export function updateStatus(
- api: Client,
- orgId: string,
- alertId: string,
- status: IncidentStatus
- ): Promise<Incident> {
- return api.requestPromise(`/organizations/${orgId}/incidents/${alertId}/`, {
- method: 'PUT',
- data: {
- status,
- },
- });
- }
- /**
- * Is incident open?
- *
- * @param {Object} incident Incident object
- * @returns {Boolean}
- */
- export function isOpen(incident: Incident): boolean {
- switch (incident.status) {
- case IncidentStatus.CLOSED:
- return false;
- default:
- return true;
- }
- }
- export function getIncidentMetricPreset(incident: Incident) {
- const alertRule = incident?.alertRule;
- const aggregate = alertRule?.aggregate ?? '';
- const dataset = alertRule?.dataset ?? Dataset.ERRORS;
- return PRESET_AGGREGATES.find(
- p => p.validDataset.includes(dataset) && p.match.test(aggregate)
- );
- }
- /**
- * Gets start and end date query parameters from stats
- */
- export function getStartEndFromStats(stats: IncidentStats) {
- const start = getUtcDateString(stats.eventStats.data[0][0] * 1000);
- const end = getUtcDateString(
- stats.eventStats.data[stats.eventStats.data.length - 1][0] * 1000
- );
- return {start, end};
- }
- /**
- * Gets the URL for a discover view of the incident with the following default
- * parameters:
- *
- * - Ordered by the incident aggregate, descending
- * - yAxis maps to the aggregate
- * - The following fields are displayed:
- * - For Error dataset alerts: [issue, count(), count_unique(user)]
- * - For Transaction dataset alerts: [transaction, count()]
- * - Start and end are scoped to the same period as the alert rule
- */
- export function getIncidentDiscoverUrl(opts: {
- orgSlug: string;
- projects: Project[];
- incident?: Incident;
- stats?: IncidentStats;
- extraQueryParams?: Partial<NewQuery>;
- }) {
- const {orgSlug, projects, incident, stats, extraQueryParams} = opts;
- if (!projects || !projects.length || !incident || !stats) {
- return '';
- }
- const timeWindowString = `${incident.alertRule.timeWindow}m`;
- const {start, end} = getStartEndFromStats(stats);
- const discoverQuery: NewQuery = {
- id: undefined,
- name: (incident && incident.title) || '',
- orderby: `-${getAggregateAlias(incident.alertRule.aggregate)}`,
- yAxis: incident.alertRule.aggregate ? [incident.alertRule.aggregate] : undefined,
- query: incident?.discoverQuery ?? '',
- projects: projects
- .filter(({slug}) => incident.projects.includes(slug))
- .map(({id}) => Number(id)),
- version: 2,
- fields:
- incident.alertRule.dataset === Dataset.ERRORS
- ? ['issue', 'count()', 'count_unique(user)']
- : ['transaction', incident.alertRule.aggregate],
- start,
- end,
- ...extraQueryParams,
- };
- const discoverView = EventView.fromSavedQuery(discoverQuery);
- const {query, ...toObject} = discoverView.getResultsViewUrlTarget(orgSlug);
- return {
- query: {...query, interval: timeWindowString},
- ...toObject,
- };
- }
- export function isIssueAlert(
- data: IssueAlertRule | SavedIncidentRule | IncidentRule
- ): data is IssueAlertRule {
- return !data.hasOwnProperty('triggers');
- }
- export const DATA_SOURCE_LABELS = {
- [Dataset.ERRORS]: t('Errors'),
- [Dataset.TRANSACTIONS]: t('Transactions'),
- [Datasource.ERROR_DEFAULT]: t('event.type:error OR event.type:default'),
- [Datasource.ERROR]: t('event.type:error'),
- [Datasource.DEFAULT]: t('event.type:default'),
- [Datasource.TRANSACTION]: t('event.type:transaction'),
- };
- // Maps a datasource to the relevant dataset and event_types for the backend to use
- export const DATA_SOURCE_TO_SET_AND_EVENT_TYPES = {
- [Datasource.ERROR_DEFAULT]: {
- dataset: Dataset.ERRORS,
- eventTypes: [EventTypes.ERROR, EventTypes.DEFAULT],
- },
- [Datasource.ERROR]: {
- dataset: Dataset.ERRORS,
- eventTypes: [EventTypes.ERROR],
- },
- [Datasource.DEFAULT]: {
- dataset: Dataset.ERRORS,
- eventTypes: [EventTypes.DEFAULT],
- },
- [Datasource.TRANSACTION]: {
- dataset: Dataset.TRANSACTIONS,
- eventTypes: [EventTypes.TRANSACTION],
- },
- };
- // Converts the given dataset and event types array to a datasource for the datasource dropdown
- export function convertDatasetEventTypesToSource(
- dataset: Dataset,
- eventTypes: EventTypes[]
- ) {
- // transactions only has one datasource option regardless of event type
- if (dataset === Dataset.TRANSACTIONS) {
- return Datasource.TRANSACTION;
- }
- // if no event type was provided use the default datasource
- if (!eventTypes) {
- return Datasource.ERROR;
- }
- if (eventTypes.includes(EventTypes.DEFAULT) && eventTypes.includes(EventTypes.ERROR)) {
- return Datasource.ERROR_DEFAULT;
- } else if (eventTypes.includes(EventTypes.DEFAULT)) {
- return Datasource.DEFAULT;
- } else {
- return Datasource.ERROR;
- }
- }
- /**
- * Attempt to guess the data source of a discover query
- *
- * @returns An object containing the datasource and new query without the datasource.
- * Returns null on no datasource.
- */
- export function getQueryDatasource(
- query: string
- ): {source: Datasource; query: string} | null {
- let match = query.match(
- /\(?\bevent\.type:(error|default|transaction)\)?\WOR\W\(?event\.type:(error|default|transaction)\)?/i
- );
- if (match) {
- // should be [error, default] or [default, error]
- const eventTypes = match.slice(1, 3).sort().join(',');
- if (eventTypes !== 'default,error') {
- return null;
- }
- return {source: Datasource.ERROR_DEFAULT, query: query.replace(match[0], '').trim()};
- }
- match = query.match(/(^|\s)event\.type:(error|default|transaction)/i);
- if (match && Datasource[match[2].toUpperCase()]) {
- return {
- source: Datasource[match[2].toUpperCase()],
- query: query.replace(match[0], '').trim(),
- };
- }
- return null;
- }
- export function isSessionAggregate(aggregate: string) {
- return Object.values(SessionsAggregate).includes(aggregate as SessionsAggregate);
- }
- export const SESSION_AGGREGATE_TO_FIELD = {
- [SessionsAggregate.CRASH_FREE_SESSIONS]: SessionField.SESSIONS,
- [SessionsAggregate.CRASH_FREE_USERS]: SessionField.USERS,
- };
- export function alertAxisFormatter(value: number, seriesName: string, aggregate: string) {
- if (isSessionAggregate(aggregate)) {
- return defined(value) ? `${round(value, 2)}%` : '\u2015';
- }
- return axisLabelFormatter(value, seriesName);
- }
- export function alertTooltipValueFormatter(
- value: number,
- seriesName: string,
- aggregate: string
- ) {
- if (isSessionAggregate(aggregate)) {
- return defined(value) ? `${value}%` : '\u2015';
- }
- return tooltipFormatter(value, seriesName);
- }
- export const ALERT_CHART_MIN_MAX_BUFFER = 1.03;
- export function shouldScaleAlertChart(aggregate: string) {
- // We want crash free rate charts to be scaled because they are usually too
- // close to 100% and therefore too fine to see the spikes on 0%-100% scale.
- return isSessionAggregate(aggregate);
- }
|