virtualMetricsContext.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import {createContext, useCallback, useContext, useMemo} from 'react';
  2. import type {
  3. MetricAggregation,
  4. MetricMeta,
  5. MetricsExtractionCondition,
  6. MetricsExtractionRule,
  7. MRI,
  8. } from 'sentry/types/metrics';
  9. import {
  10. aggregationToMetricType,
  11. BUILT_IN_CONDITION_ID,
  12. } from 'sentry/utils/metrics/extractionRules';
  13. import {DEFAULT_MRI, formatMRI, parseMRI} from 'sentry/utils/metrics/mri';
  14. import type {MetricTag} from 'sentry/utils/metrics/types';
  15. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  16. import {useApiQuery} from 'sentry/utils/queryClient';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. interface ContextType {
  20. getAggregations: (mri: MRI, conditionId: number) => MetricAggregation[];
  21. getCondition: (mri: MRI, conditionId: number) => MetricsExtractionCondition | null;
  22. getConditions: (mri: MRI) => MetricsExtractionCondition[];
  23. getExtractionRule: (mri: MRI, conditionId: number) => MetricsExtractionRule | null;
  24. getExtractionRules: (mri: MRI) => MetricsExtractionRule[];
  25. getTags: (mri: MRI, conditionId: number) => MetricTag[];
  26. getVirtualMRI: (mri: MRI) => MRI | null;
  27. getVirtualMRIQuery: (
  28. mri: MRI,
  29. aggregation: MetricAggregation
  30. ) => {
  31. aggregation: MetricAggregation;
  32. conditionId: number;
  33. mri: MRI;
  34. } | null;
  35. getVirtualMeta: (mri: MRI) => MetricMeta;
  36. isLoading: boolean;
  37. resolveVirtualMRI: (
  38. mri: MRI,
  39. conditionId: number,
  40. aggregation: MetricAggregation
  41. ) => {aggregation: MetricAggregation; mri: MRI};
  42. virtualMeta: MetricMeta[];
  43. }
  44. const Context = createContext<ContextType>({
  45. getAggregations: () => [],
  46. getVirtualMRI: () => null,
  47. getVirtualMeta: () => {
  48. throw new Error('Not implemented');
  49. },
  50. getConditions: () => [],
  51. getCondition: () => null,
  52. getExtractionRule: () => null,
  53. getExtractionRules: () => [],
  54. getTags: () => [],
  55. getVirtualMRIQuery: () => null,
  56. resolveVirtualMRI: (mri, _, aggregation) => ({mri, aggregation}),
  57. virtualMeta: [],
  58. isLoading: false,
  59. });
  60. export function useVirtualMetricsContext() {
  61. return useContext(Context);
  62. }
  63. interface Props {
  64. children: React.ReactNode;
  65. }
  66. export function createVirtualMRI(rule: MetricsExtractionRule): MRI {
  67. return `v:custom/${rule.spanAttribute}@none`;
  68. }
  69. export function createMRIToVirtualMap(rules: MetricsExtractionRule[]): Map<MRI, MRI> {
  70. const mriMap = new Map<MRI, MRI>();
  71. for (const rule of rules) {
  72. for (const condition of rule.conditions) {
  73. for (const mri of condition.mris) {
  74. mriMap.set(mri, createVirtualMRI(rule));
  75. }
  76. }
  77. }
  78. return mriMap;
  79. }
  80. const getMetricsExtractionRulesApiKey = (orgSlug: string, projects: number[]) =>
  81. [
  82. `/organizations/${orgSlug}/metrics/extraction-rules/`,
  83. {
  84. query: {
  85. project: projects,
  86. },
  87. },
  88. ] as const;
  89. const EMPTY_ARRAY: never[] = [];
  90. function stripBuiltInCondition(rule: MetricsExtractionRule): MetricsExtractionRule {
  91. return {
  92. ...rule,
  93. conditions: rule.conditions.filter(
  94. condition => condition.id !== BUILT_IN_CONDITION_ID
  95. ),
  96. };
  97. }
  98. export function VirtualMetricsContextProvider({children}: Props) {
  99. const organization = useOrganization();
  100. const {selection, isReady} = usePageFilters();
  101. const extractionRulesQuery = useApiQuery<MetricsExtractionRule[]>(
  102. getMetricsExtractionRulesApiKey(organization.slug, selection.projects),
  103. {staleTime: 0, enabled: isReady}
  104. );
  105. const spanMetaQuery = useMetricsMeta(selection, ['spans']);
  106. const extractionRules = extractionRulesQuery.data ?? EMPTY_ARRAY;
  107. const isLoading = extractionRulesQuery.isLoading || spanMetaQuery.isLoading;
  108. const extractionRulesWithBuiltIn = useMemo(() => {
  109. return extractionRules.map(rule => {
  110. const matchingBuiltInMetric = spanMetaQuery.data.find(
  111. meta => formatMRI(meta.mri) === rule.spanAttribute
  112. );
  113. if (!matchingBuiltInMetric) {
  114. return rule;
  115. }
  116. return {
  117. ...rule,
  118. conditions: [
  119. {
  120. id: BUILT_IN_CONDITION_ID,
  121. mris: [matchingBuiltInMetric.mri],
  122. value: '',
  123. },
  124. ...rule.conditions,
  125. ],
  126. };
  127. });
  128. }, [extractionRules, spanMetaQuery.data]);
  129. const mriToVirtualMap = useMemo(
  130. () => createMRIToVirtualMap(extractionRulesWithBuiltIn),
  131. [extractionRulesWithBuiltIn]
  132. );
  133. const virtualMRIToRuleMap = useMemo(() => {
  134. const map = new Map<MRI, MetricsExtractionRule[]>();
  135. extractionRulesWithBuiltIn.forEach(rule => {
  136. const virtualMRI = createVirtualMRI(rule);
  137. const existingRules = map.get(virtualMRI) || [];
  138. map.set(virtualMRI, [...existingRules, rule]);
  139. });
  140. return map;
  141. }, [extractionRulesWithBuiltIn]);
  142. const getVirtualMRI = useCallback(
  143. (mri: MRI): MRI | null => {
  144. const virtualMRI = mriToVirtualMap.get(mri);
  145. if (!virtualMRI) {
  146. return null;
  147. }
  148. return virtualMRI;
  149. },
  150. [mriToVirtualMap]
  151. );
  152. const getVirtualMeta = useCallback(
  153. (mri: MRI): MetricMeta => {
  154. const rules = virtualMRIToRuleMap.get(mri);
  155. if (!rules) {
  156. throw new Error('Rules not found');
  157. }
  158. return {
  159. type: 'v',
  160. unit: 'none',
  161. blockingStatus: [],
  162. mri: mri,
  163. operations: [],
  164. projectIds: rules.map(rule => rule.projectId),
  165. };
  166. },
  167. [virtualMRIToRuleMap]
  168. );
  169. const getConditions = useCallback(
  170. (mri: MRI): MetricsExtractionCondition[] => {
  171. const rules = virtualMRIToRuleMap.get(mri);
  172. const conditions = rules?.flatMap(rule => rule.conditions) ?? [];
  173. // Unique by id
  174. return Array.from(
  175. new Map(conditions.map(condition => [condition.id, condition])).values()
  176. );
  177. },
  178. [virtualMRIToRuleMap]
  179. );
  180. const getCondition = useCallback(
  181. (mri: MRI, conditionId: number) => {
  182. const conditions = getConditions(mri);
  183. return conditions.find(c => c.id === conditionId) || null;
  184. },
  185. [getConditions]
  186. );
  187. const getExtractionRules = useCallback(
  188. (mri: MRI) => {
  189. return virtualMRIToRuleMap.get(mri)?.map(stripBuiltInCondition) ?? [];
  190. },
  191. [virtualMRIToRuleMap]
  192. );
  193. const getExtractionRule = useCallback(
  194. (mri: MRI, conditionId: number) => {
  195. const rules = getExtractionRules(mri);
  196. if (!rules) {
  197. return null;
  198. }
  199. const rule = rules.find(r => r.conditions.some(c => c.id === conditionId));
  200. if (!rule) {
  201. return null;
  202. }
  203. return {
  204. ...rule,
  205. // return the original rule without the built-in condition
  206. conditions: rule.conditions.filter(
  207. condition => condition.id !== BUILT_IN_CONDITION_ID
  208. ),
  209. };
  210. },
  211. [getExtractionRules]
  212. );
  213. const getTags = useCallback(
  214. (mri: MRI, conditionId: number): MetricTag[] => {
  215. const rule = getExtractionRule(mri, conditionId);
  216. return rule?.tags.map(tag => ({key: tag})) || [];
  217. },
  218. [getExtractionRule]
  219. );
  220. const resolveVirtualMRI = useCallback(
  221. (
  222. mri: MRI,
  223. conditionId: number,
  224. aggregation: MetricAggregation
  225. ): {
  226. aggregation: MetricAggregation;
  227. mri: MRI;
  228. } => {
  229. const condition = getCondition(mri, conditionId);
  230. if (!condition) {
  231. return {mri: DEFAULT_MRI, aggregation: 'sum'};
  232. }
  233. if (conditionId === BUILT_IN_CONDITION_ID) {
  234. // TODO: Do we need to check the aggregate?
  235. return {mri: condition.mris[0], aggregation};
  236. }
  237. const metricType = aggregationToMetricType[aggregation];
  238. let resolvedMRI = condition.mris.find(m => m.startsWith(metricType));
  239. if (!resolvedMRI) {
  240. resolvedMRI = mri;
  241. }
  242. return {mri: resolvedMRI, aggregation: metricType === 'c' ? 'sum' : aggregation};
  243. },
  244. [getCondition]
  245. );
  246. const getVirtualMRIQuery = useCallback(
  247. (
  248. mri: MRI,
  249. aggregation: MetricAggregation
  250. ): {
  251. aggregation: MetricAggregation;
  252. conditionId: number;
  253. mri: MRI;
  254. } | null => {
  255. const virtualMRI = getVirtualMRI(mri);
  256. if (!virtualMRI) {
  257. return null;
  258. }
  259. const condition = getConditions(virtualMRI).find(c => c.mris.includes(mri));
  260. if (!condition) {
  261. return null;
  262. }
  263. return {
  264. mri: virtualMRI,
  265. conditionId: condition.id,
  266. aggregation: parseMRI(mri).type === 'c' ? 'count' : aggregation,
  267. };
  268. },
  269. [getVirtualMRI, getConditions]
  270. );
  271. const getAggregations = useCallback(
  272. (mri: MRI, conditionId: number) => {
  273. if (conditionId === BUILT_IN_CONDITION_ID) {
  274. const condition = getCondition(mri, conditionId);
  275. const builtInMeta = spanMetaQuery.data.find(
  276. meta => meta.mri === condition?.mris[0]
  277. );
  278. return builtInMeta?.operations ?? [];
  279. }
  280. const rule = getExtractionRule(mri, conditionId);
  281. if (!rule) {
  282. return [];
  283. }
  284. return rule.aggregates;
  285. },
  286. [getCondition, getExtractionRule, spanMetaQuery.data]
  287. );
  288. const virtualMeta = useMemo(
  289. () => Array.from(virtualMRIToRuleMap.keys()).map(getVirtualMeta),
  290. [getVirtualMeta, virtualMRIToRuleMap]
  291. );
  292. const contextValue = useMemo<ContextType>(
  293. () => ({
  294. getVirtualMRI,
  295. getVirtualMeta,
  296. getConditions,
  297. getCondition,
  298. getAggregations,
  299. getExtractionRule,
  300. getExtractionRules,
  301. getTags,
  302. getVirtualMRIQuery,
  303. resolveVirtualMRI,
  304. virtualMeta,
  305. isLoading,
  306. }),
  307. [
  308. getVirtualMRI,
  309. getVirtualMeta,
  310. getConditions,
  311. getCondition,
  312. getAggregations,
  313. getExtractionRule,
  314. getExtractionRules,
  315. getTags,
  316. getVirtualMRIQuery,
  317. resolveVirtualMRI,
  318. virtualMeta,
  319. isLoading,
  320. ]
  321. );
  322. return <Context.Provider value={contextValue}>{children}</Context.Provider>;
  323. }