index.tsx 6.6 KB

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