index.tsx 7.1 KB

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