index.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import round from 'lodash/round';
  2. import {t} from 'sentry/locale';
  3. import type {Organization} from 'sentry/types';
  4. import {SessionFieldWithOperation} from 'sentry/types';
  5. import type {IssueAlertRule} from 'sentry/types/alerts';
  6. import {defined} from 'sentry/utils';
  7. import {getUtcDateString} from 'sentry/utils/dates';
  8. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  9. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  10. import {formatMetricUsingFixedUnit} from 'sentry/utils/metrics/formatters';
  11. import {parseField, parseMRI} from 'sentry/utils/metrics/mri';
  12. import toArray from 'sentry/utils/toArray';
  13. import type {MetricRule, SavedMetricRule} from 'sentry/views/alerts/rules/metric/types';
  14. import {
  15. Dataset,
  16. Datasource,
  17. EventTypes,
  18. SessionsAggregate,
  19. } from 'sentry/views/alerts/rules/metric/types';
  20. import {isCustomMetricAlert} from 'sentry/views/alerts/rules/metric/utils/isCustomMetricAlert';
  21. import type {Incident, IncidentStats} from '../types';
  22. import {AlertRuleStatus} from '../types';
  23. /**
  24. * Gets start and end date query parameters from stats
  25. */
  26. export function getStartEndFromStats(stats: IncidentStats) {
  27. const start = getUtcDateString(stats.eventStats.data[0][0] * 1000);
  28. const end = getUtcDateString(
  29. stats.eventStats.data[stats.eventStats.data.length - 1][0] * 1000
  30. );
  31. return {start, end};
  32. }
  33. export function isIssueAlert(
  34. data: IssueAlertRule | SavedMetricRule | MetricRule
  35. ): data is IssueAlertRule {
  36. return !data.hasOwnProperty('triggers');
  37. }
  38. export const DATA_SOURCE_LABELS = {
  39. [Dataset.ERRORS]: t('Errors'),
  40. [Dataset.TRANSACTIONS]: t('Transactions'),
  41. [Datasource.ERROR_DEFAULT]: 'event.type:error OR event.type:default',
  42. [Datasource.ERROR]: 'event.type:error',
  43. [Datasource.DEFAULT]: 'event.type:default',
  44. [Datasource.TRANSACTION]: 'event.type:transaction',
  45. };
  46. // Maps a datasource to the relevant dataset and event_types for the backend to use
  47. export const DATA_SOURCE_TO_SET_AND_EVENT_TYPES = {
  48. [Datasource.ERROR_DEFAULT]: {
  49. dataset: Dataset.ERRORS,
  50. eventTypes: [EventTypes.ERROR, EventTypes.DEFAULT],
  51. },
  52. [Datasource.ERROR]: {
  53. dataset: Dataset.ERRORS,
  54. eventTypes: [EventTypes.ERROR],
  55. },
  56. [Datasource.DEFAULT]: {
  57. dataset: Dataset.ERRORS,
  58. eventTypes: [EventTypes.DEFAULT],
  59. },
  60. [Datasource.TRANSACTION]: {
  61. dataset: Dataset.TRANSACTIONS,
  62. eventTypes: [EventTypes.TRANSACTION],
  63. },
  64. };
  65. // Converts the given dataset and event types array to a datasource for the datasource dropdown
  66. export function convertDatasetEventTypesToSource(
  67. dataset: Dataset,
  68. eventTypes: EventTypes[]
  69. ) {
  70. // transactions and generic_metrics only have one datasource option regardless of event type
  71. if (dataset === Dataset.TRANSACTIONS || dataset === Dataset.GENERIC_METRICS) {
  72. return Datasource.TRANSACTION;
  73. }
  74. // if no event type was provided use the default datasource
  75. if (!eventTypes) {
  76. return Datasource.ERROR;
  77. }
  78. if (eventTypes.includes(EventTypes.DEFAULT) && eventTypes.includes(EventTypes.ERROR)) {
  79. return Datasource.ERROR_DEFAULT;
  80. }
  81. if (eventTypes.includes(EventTypes.DEFAULT)) {
  82. return Datasource.DEFAULT;
  83. }
  84. return Datasource.ERROR;
  85. }
  86. /**
  87. * Attempt to guess the data source of a discover query
  88. *
  89. * @returns An object containing the datasource and new query without the datasource.
  90. * Returns null on no datasource.
  91. */
  92. export function getQueryDatasource(
  93. query: string
  94. ): {query: string; source: Datasource} | null {
  95. let match = query.match(
  96. /\(?\bevent\.type:(error|default|transaction)\)?\WOR\W\(?event\.type:(error|default|transaction)\)?/i
  97. );
  98. if (match) {
  99. // should be [error, default] or [default, error]
  100. const eventTypes = match.slice(1, 3).sort().join(',');
  101. if (eventTypes !== 'default,error') {
  102. return null;
  103. }
  104. return {source: Datasource.ERROR_DEFAULT, query: query.replace(match[0], '').trim()};
  105. }
  106. match = query.match(/(^|\s)event\.type:(error|default|transaction)/i);
  107. if (match && Datasource[match[2].toUpperCase()]) {
  108. return {
  109. source: Datasource[match[2].toUpperCase()],
  110. query: query.replace(match[0], '').trim(),
  111. };
  112. }
  113. return null;
  114. }
  115. export function isSessionAggregate(aggregate: string) {
  116. return Object.values(SessionsAggregate).includes(aggregate as SessionsAggregate);
  117. }
  118. export const SESSION_AGGREGATE_TO_FIELD = {
  119. [SessionsAggregate.CRASH_FREE_SESSIONS]: SessionFieldWithOperation.SESSIONS,
  120. [SessionsAggregate.CRASH_FREE_USERS]: SessionFieldWithOperation.USERS,
  121. };
  122. export function alertAxisFormatter(value: number, seriesName: string, aggregate: string) {
  123. if (isSessionAggregate(aggregate)) {
  124. return defined(value) ? `${round(value, 2)}%` : '\u2015';
  125. }
  126. if (isCustomMetricAlert(aggregate)) {
  127. const {mri, op} = parseField(aggregate)!;
  128. const {unit} = parseMRI(mri)!;
  129. return formatMetricUsingFixedUnit(value, unit, op);
  130. }
  131. return axisLabelFormatter(value, aggregateOutputType(seriesName));
  132. }
  133. export function alertTooltipValueFormatter(
  134. value: number,
  135. seriesName: string,
  136. aggregate: string
  137. ) {
  138. if (isSessionAggregate(aggregate)) {
  139. return defined(value) ? `${value}%` : '\u2015';
  140. }
  141. if (isCustomMetricAlert(aggregate)) {
  142. const {mri, op} = parseField(aggregate)!;
  143. const {unit} = parseMRI(mri)!;
  144. return formatMetricUsingFixedUnit(value, unit, op);
  145. }
  146. return tooltipFormatter(value, aggregateOutputType(seriesName));
  147. }
  148. export const ALERT_CHART_MIN_MAX_BUFFER = 1.03;
  149. export function shouldScaleAlertChart(aggregate: string) {
  150. // We want crash free rate charts to be scaled because they are usually too
  151. // close to 100% and therefore too fine to see the spikes on 0%-100% scale.
  152. return isSessionAggregate(aggregate);
  153. }
  154. export function alertDetailsLink(organization: Organization, incident: Incident) {
  155. return `/organizations/${organization.slug}/alerts/rules/details/${
  156. incident.alertRule.status === AlertRuleStatus.SNAPSHOT &&
  157. incident.alertRule.originalAlertRuleId
  158. ? incident.alertRule.originalAlertRuleId
  159. : incident.alertRule.id
  160. }/`;
  161. }
  162. /**
  163. * Noramlizes a status string
  164. */
  165. export function getQueryStatus(status: string | string[]): string {
  166. if (Array.isArray(status) || status === '') {
  167. return 'all';
  168. }
  169. return ['open', 'closed'].includes(status) ? status : 'all';
  170. }
  171. const ALERT_LIST_QUERY_DEFAULT_TEAMS = ['myteams', 'unassigned'];
  172. /**
  173. * Noramlize a team slug from the query
  174. */
  175. export function getTeamParams(team?: string | string[]): string[] {
  176. if (team === undefined) {
  177. return ALERT_LIST_QUERY_DEFAULT_TEAMS;
  178. }
  179. if (team === '') {
  180. return [];
  181. }
  182. return toArray(team);
  183. }