spanMetricsField.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import SelectControl from 'sentry/components/forms/controls/selectControl';
  4. import {Tooltip} from 'sentry/components/tooltip';
  5. import {t} from 'sentry/locale';
  6. import type {
  7. MetricAggregation,
  8. MetricsExtractionCondition,
  9. MetricsExtractionRule,
  10. MRI,
  11. } from 'sentry/types/metrics';
  12. import type {Project} from 'sentry/types/project';
  13. import {isCounterMetric} from 'sentry/utils/metrics';
  14. import {aggregationToMetricType} from 'sentry/utils/metrics/extractionRules';
  15. import {MRIToField, parseField} from 'sentry/utils/metrics/mri';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import {useMetricsExtractionRules} from 'sentry/views/settings/projectMetrics/utils/useMetricsExtractionRules';
  18. interface Props {
  19. field: string;
  20. onChange: (value: string, meta: Record<string, any>) => void;
  21. project: Project;
  22. }
  23. function findMriForAggregate(
  24. condition: MetricsExtractionCondition | undefined,
  25. aggregate: MetricAggregation
  26. ) {
  27. const requestedType = aggregationToMetricType[aggregate];
  28. return condition?.mris.find(mri => mri.startsWith(requestedType));
  29. }
  30. function SpanMetricField({field, project, onChange}: Props) {
  31. const organization = useOrganization();
  32. const {data: extractionRules, isLoading} = useMetricsExtractionRules({
  33. orgId: organization.slug,
  34. projectId: project.id,
  35. });
  36. const parsedField = useMemo(() => parseField(field), [field]);
  37. const selectedAggregate =
  38. // Internally we use `sum` for counter metrics but expose `count` to the user
  39. parsedField?.aggregation === 'sum' ? 'count' : parsedField?.aggregation;
  40. const [selectedRule, selectedCondition] = useMemo(() => {
  41. if (!extractionRules || !parsedField) {
  42. return [null, null];
  43. }
  44. let rule: MetricsExtractionRule | null = null;
  45. let condition: MetricsExtractionCondition | null = null;
  46. for (const currentRule of extractionRules || []) {
  47. for (const currentCondition of currentRule.conditions) {
  48. if (currentCondition.mris.includes(parsedField.mri)) {
  49. rule = currentRule;
  50. condition = currentCondition;
  51. break;
  52. }
  53. }
  54. if (rule) {
  55. break;
  56. }
  57. }
  58. return [rule, condition];
  59. }, [extractionRules, parsedField]);
  60. const attributeOptions = useMemo(() => {
  61. return (
  62. extractionRules
  63. ?.map(rule => ({
  64. label: rule.spanAttribute,
  65. value: rule.spanAttribute,
  66. }))
  67. .sort((a, b) => a.label.localeCompare(b.label)) ?? []
  68. );
  69. }, [extractionRules]);
  70. const aggregateOptions = useMemo(() => {
  71. return (
  72. selectedRule?.aggregates.map(agg => ({
  73. label: agg,
  74. value: agg,
  75. })) ?? []
  76. );
  77. }, [selectedRule]);
  78. const conditionOptions = useMemo(() => {
  79. return selectedRule?.conditions.map(condition => ({
  80. label: condition.value ? (
  81. <Tooltip showOnlyOnOverflow title={condition.value} skipWrapper>
  82. <ConditionLabel>{condition.value}</ConditionLabel>
  83. </Tooltip>
  84. ) : (
  85. t('All spans')
  86. ),
  87. value: condition.id,
  88. }));
  89. }, [selectedRule]);
  90. const handleChange = useCallback(
  91. (newMRI: MRI, newAggregate: MetricAggregation) => {
  92. if (isCounterMetric({mri: newMRI})) {
  93. // We expose `count` to the user but the internal aggregation for a counter metric is `sum`
  94. onChange(MRIToField(newMRI, 'sum'), {});
  95. return;
  96. }
  97. onChange(MRIToField(newMRI, newAggregate), {});
  98. },
  99. [onChange]
  100. );
  101. const handleMriChange = useCallback(
  102. option => {
  103. const newRule = extractionRules?.find(rule => rule.spanAttribute === option.value);
  104. if (!newRule) {
  105. return;
  106. }
  107. const newAggregate = newRule.aggregates[0];
  108. if (!newAggregate) {
  109. // Encoutered invalid extraction rule
  110. return;
  111. }
  112. const newMRI = findMriForAggregate(newRule.conditions[0], newAggregate);
  113. if (!newMRI) {
  114. // Encoutered invalid extraction rule
  115. return;
  116. }
  117. handleChange(newMRI, newAggregate);
  118. },
  119. [extractionRules, handleChange]
  120. );
  121. const handleConditionChange = useCallback(
  122. option => {
  123. if (!selectedRule || !selectedAggregate) {
  124. return;
  125. }
  126. const newCondition = selectedRule.conditions.find(
  127. condition => condition.id === option.value
  128. );
  129. if (!newCondition) {
  130. return;
  131. }
  132. // Find an MRI for the currently selected aggregate
  133. const newMRI = findMriForAggregate(newCondition, selectedAggregate);
  134. if (!newMRI) {
  135. // Encoutered invalid extraction rule
  136. return;
  137. }
  138. handleChange(newMRI, selectedAggregate);
  139. },
  140. [handleChange, selectedAggregate, selectedRule]
  141. );
  142. const handleAggregateChange = useCallback(
  143. option => {
  144. if (!selectedCondition) {
  145. return;
  146. }
  147. const newMRI = findMriForAggregate(selectedCondition, option.value);
  148. if (!newMRI) {
  149. return;
  150. }
  151. handleChange(newMRI, option.value);
  152. },
  153. [handleChange, selectedCondition]
  154. );
  155. return (
  156. <Fragment>
  157. <SelectControl
  158. searchable
  159. isDisabled={isLoading}
  160. placeholder={t('Select a metric')}
  161. noOptionsMessage={() =>
  162. attributeOptions.length === 0
  163. ? t('No span metrics in this project')
  164. : t('No options')
  165. }
  166. options={attributeOptions}
  167. filterOption={() => true}
  168. value={selectedRule?.spanAttribute}
  169. onChange={handleMriChange}
  170. />
  171. <SelectControl
  172. searchable
  173. isDisabled={isLoading || !selectedRule}
  174. placeholder={t('Select a filter')}
  175. options={conditionOptions}
  176. value={selectedCondition?.id}
  177. onChange={handleConditionChange}
  178. />
  179. <SelectControl
  180. searchable
  181. isDisabled={isLoading || !selectedRule}
  182. placeholder={t('Select an aggregate')}
  183. options={aggregateOptions}
  184. value={selectedAggregate}
  185. onChange={handleAggregateChange}
  186. />
  187. </Fragment>
  188. );
  189. }
  190. export default SpanMetricField;
  191. const ConditionLabel = styled('code')`
  192. padding-left: 0;
  193. max-width: 350px;
  194. ${p => p.theme.overflowEllipsis}
  195. `;