mri.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import {t} from 'sentry/locale';
  2. import type {
  3. MetricAggregation,
  4. MetricType,
  5. MRI,
  6. ParsedMRI,
  7. UseCase,
  8. } from 'sentry/types/metrics';
  9. import {parseFunction} from 'sentry/utils/discover/fields';
  10. export const DEFAULT_MRI: MRI = 'c:custom/sentry_metric@none';
  11. export const DEFAULT_SPAN_MRI: MRI = 'c:custom/span_attribute_0@none';
  12. // This is a workaround as the alert builder requires a valid aggregate to be set
  13. export const DEFAULT_METRIC_ALERT_FIELD = `sum(${DEFAULT_MRI})`;
  14. export const DEFAULT_SPAN_METRIC_ALERT_FIELD = `sum(${DEFAULT_SPAN_MRI})`;
  15. export function isMRI(mri?: unknown): mri is MRI {
  16. if (typeof mri !== 'string') {
  17. return false;
  18. }
  19. try {
  20. _parseMRI(mri);
  21. return true;
  22. } catch {
  23. return false;
  24. }
  25. }
  26. type ParseResult<T extends MRI | string | null> = T extends MRI
  27. ? ParsedMRI
  28. : ParsedMRI | null;
  29. export function parseMRI<T extends MRI | string | null>(mri?: T): ParseResult<T> {
  30. if (!mri) {
  31. // TODO: How can this be done without casting?
  32. return null as ParseResult<T>;
  33. }
  34. try {
  35. return _parseMRI(mri) as ParseResult<T>;
  36. } catch {
  37. return null as ParseResult<T>;
  38. }
  39. }
  40. function _parseMRI(mri: string): ParsedMRI {
  41. const mriArray = mri.split(new RegExp(/[:/@]/));
  42. if (mriArray.length !== 4) {
  43. throw new Error('Invalid MRI');
  44. }
  45. const [metricType, useCase, name, unit] = mriArray;
  46. return {
  47. type: metricType as MetricType,
  48. name: parseName(name, useCase as UseCase),
  49. unit,
  50. useCase: useCase as UseCase,
  51. };
  52. }
  53. function parseName(name: string, useCase: UseCase): string {
  54. if (useCase === 'custom') {
  55. return name;
  56. }
  57. if (useCase === 'transactions') {
  58. if (name === 'duration') {
  59. return 'transaction.duration';
  60. }
  61. return name;
  62. }
  63. if (useCase === 'spans') {
  64. if (['duration', 'self_time', 'exclusive_time'].includes(name)) {
  65. return `span.${name}`;
  66. }
  67. return name;
  68. }
  69. return `${useCase}.${name}`;
  70. }
  71. export function toMRI({type, useCase, name, unit}: ParsedMRI): MRI {
  72. return `${type}:${useCase}/${name}@${unit}`;
  73. }
  74. export function formatMRI(mri: MRI): string {
  75. const parsedMRI = parseMRI(mri);
  76. if (parsedMRI?.type !== 'v') {
  77. return parsedMRI?.name;
  78. }
  79. return parsedMRI.name.split('|')[0];
  80. }
  81. export function getUseCaseFromMRI(mri?: string): UseCase | undefined {
  82. const parsed = parseMRI(mri);
  83. return parsed?.useCase;
  84. }
  85. export function MRIToField(mri: MRI, aggregation: MetricAggregation): string {
  86. return `${aggregation}(${mri})`;
  87. }
  88. export function parseField(
  89. field: string
  90. ): {aggregation: MetricAggregation; mri: MRI} | null {
  91. const parsedFunction = parseFunction(field);
  92. if (!parsedFunction) {
  93. return null;
  94. }
  95. return {
  96. mri: parsedFunction.arguments[0] as MRI,
  97. aggregation: parsedFunction.name as MetricAggregation,
  98. };
  99. }
  100. export function isMRIField(field: string): boolean {
  101. return !!parseMRI(parseField(field)?.mri);
  102. }
  103. // convenience function to get the MRI from a field, returns defaut MRI if it fails
  104. export function getMRI(field: string): MRI {
  105. const parsed = parseField(field);
  106. return parsed?.mri ?? DEFAULT_MRI;
  107. }
  108. export function formatMRIField(aggregate: string) {
  109. if (
  110. aggregate === DEFAULT_METRIC_ALERT_FIELD ||
  111. aggregate === DEFAULT_SPAN_METRIC_ALERT_FIELD
  112. ) {
  113. return t('Select a metric to get started');
  114. }
  115. const parsed = parseField(aggregate);
  116. // The field does not contain an MRI -> return the aggregate as is
  117. if (!parsed || !parsed.mri) {
  118. return aggregate;
  119. }
  120. return `${parsed.aggregation}(${formatMRI(parsed.mri)})`;
  121. }
  122. export function isExtractedCustomMetric({mri}: {mri: MRI}) {
  123. // Extraced metrics are prefixed with `span_attribute_`
  124. return mri.substring(1).startsWith(':custom/span_attribute_');
  125. }