useMetricsExtractionRules.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import type {MetricsExtractionRule} from 'sentry/types/metrics';
  2. import {
  3. type ApiQueryKey,
  4. getApiQueryData,
  5. type QueryClient,
  6. setApiQueryData,
  7. useApiQuery,
  8. type UseApiQueryOptions,
  9. useMutation,
  10. useQueryClient,
  11. } from 'sentry/utils/queryClient';
  12. import type RequestError from 'sentry/utils/requestError/requestError';
  13. import useApi from 'sentry/utils/useApi';
  14. /**
  15. * Remove temporary ids from conditions before sending to the server
  16. */
  17. function filterTempIds(rules: MetricsExtractionRule[]) {
  18. return rules.map(rule => ({
  19. ...rule,
  20. conditions: rule.conditions.map(condition => ({
  21. ...condition,
  22. id: condition.id < 0 ? undefined : condition.id,
  23. })),
  24. }));
  25. }
  26. interface MetricRulesAPIQueryParams {
  27. query?: string;
  28. }
  29. export const getMetricsExtractionRulesApiKey = (
  30. orgId: string | number,
  31. projectId?: string | number,
  32. query?: MetricRulesAPIQueryParams
  33. ) => {
  34. const endpoint = `/projects/${orgId}/${projectId}/metrics/extraction-rules/`;
  35. if (!query || Object.keys(query).length === 0) {
  36. // when no query is provided, return only endpoint path as a key
  37. return [endpoint] as const;
  38. }
  39. return [endpoint, {query: query}] as const;
  40. };
  41. export const getMetricsExtractionOrgApiKey = (orgSlug: string) =>
  42. [`/organizations/${orgSlug}/metrics/extraction-rules/`] as const;
  43. interface GetParams {
  44. orgId: string | number;
  45. projectId?: string | number;
  46. query?: MetricRulesAPIQueryParams;
  47. }
  48. export function useMetricsExtractionRules(
  49. {orgId, projectId, query}: GetParams,
  50. options: Partial<UseApiQueryOptions<MetricsExtractionRule[]>> = {}
  51. ) {
  52. return useApiQuery<MetricsExtractionRule[]>(
  53. getMetricsExtractionRulesApiKey(orgId, projectId, query),
  54. {
  55. staleTime: 0,
  56. retry: false,
  57. ...options,
  58. enabled: !!projectId && options.enabled,
  59. }
  60. );
  61. }
  62. // Rules are identified by the combination of span_attribute, type and unit
  63. function getRuleIdentifier(rule: MetricsExtractionRule) {
  64. return rule.spanAttribute + rule.unit;
  65. }
  66. function createOptimisticUpdate(
  67. queryClient: QueryClient,
  68. queryKey: ApiQueryKey,
  69. updater: (
  70. variables: {metricsExtractionRules: MetricsExtractionRule[]},
  71. old: MetricsExtractionRule[] | undefined
  72. ) => MetricsExtractionRule[] | undefined
  73. ) {
  74. return function (variables: {metricsExtractionRules: MetricsExtractionRule[]}) {
  75. queryClient.cancelQueries(queryKey);
  76. const previous = getApiQueryData<MetricsExtractionRule[]>(queryClient, queryKey);
  77. setApiQueryData<MetricsExtractionRule[] | undefined>(
  78. queryClient,
  79. queryKey,
  80. oldRules => {
  81. return updater(variables, oldRules);
  82. }
  83. );
  84. return {previous};
  85. };
  86. }
  87. function createRollback(queryClient: QueryClient, queryKey: ApiQueryKey) {
  88. return function (
  89. _error: RequestError,
  90. _variables: {metricsExtractionRules: MetricsExtractionRule[]},
  91. context?: {previous?: MetricsExtractionRule[]}
  92. ) {
  93. if (context?.previous) {
  94. setApiQueryData<MetricsExtractionRule[]>(queryClient, queryKey, context.previous);
  95. }
  96. };
  97. }
  98. export function useDeleteMetricsExtractionRules(
  99. orgSlug: string,
  100. projectId: string | number
  101. ) {
  102. const api = useApi();
  103. const queryClient = useQueryClient();
  104. const queryKey = getMetricsExtractionRulesApiKey(orgSlug, projectId);
  105. return useMutation<
  106. MetricsExtractionRule[],
  107. RequestError,
  108. {metricsExtractionRules: MetricsExtractionRule[]},
  109. {previous?: MetricsExtractionRule[]}
  110. >(
  111. data => {
  112. return api.requestPromise(queryKey[0], {
  113. method: 'DELETE',
  114. data,
  115. });
  116. },
  117. {
  118. onMutate: createOptimisticUpdate(queryClient, queryKey, (variables, old) => {
  119. const deletedRules = variables.metricsExtractionRules;
  120. const deletedKeys = new Set(deletedRules.map(getRuleIdentifier));
  121. return old?.filter(rule => !deletedKeys.has(getRuleIdentifier(rule)));
  122. }),
  123. onError: createRollback(queryClient, queryKey),
  124. onSettled: () => {
  125. queryClient.invalidateQueries(queryKey);
  126. queryClient.invalidateQueries(getMetricsExtractionOrgApiKey(orgSlug));
  127. },
  128. }
  129. );
  130. }
  131. export function useCreateMetricsExtractionRules(
  132. orgSlug: string,
  133. projectId: string | number
  134. ) {
  135. const api = useApi();
  136. const queryClient = useQueryClient();
  137. const queryKey = getMetricsExtractionRulesApiKey(orgSlug, projectId);
  138. return useMutation<
  139. MetricsExtractionRule[],
  140. RequestError,
  141. {metricsExtractionRules: MetricsExtractionRule[]},
  142. {previous?: MetricsExtractionRule[]}
  143. >(
  144. data => {
  145. return api.requestPromise(queryKey[0], {
  146. method: 'POST',
  147. data: {
  148. metricsExtractionRules: filterTempIds(data.metricsExtractionRules),
  149. },
  150. });
  151. },
  152. {
  153. onMutate: createOptimisticUpdate(queryClient, queryKey, (variables, old) => {
  154. const newRules = variables.metricsExtractionRules;
  155. const existingKeys = new Set((old ?? []).map(getRuleIdentifier));
  156. const copy = old ? [...old] : [];
  157. newRules.forEach(rule => {
  158. if (!existingKeys.has(getRuleIdentifier(rule))) {
  159. copy.push(rule);
  160. }
  161. });
  162. return copy;
  163. }),
  164. onError: createRollback(queryClient, queryKey),
  165. onSettled: () => {
  166. queryClient.invalidateQueries(queryKey);
  167. queryClient.invalidateQueries(getMetricsExtractionOrgApiKey(orgSlug));
  168. },
  169. }
  170. );
  171. }
  172. export function useUpdateMetricsExtractionRules(
  173. orgSlug: string,
  174. projectId: string | number
  175. ) {
  176. const api = useApi();
  177. const queryClient = useQueryClient();
  178. const queryKey = getMetricsExtractionRulesApiKey(orgSlug, projectId);
  179. return useMutation<
  180. MetricsExtractionRule[],
  181. RequestError,
  182. {metricsExtractionRules: MetricsExtractionRule[]},
  183. {previous?: MetricsExtractionRule[]}
  184. >(
  185. data => {
  186. return api.requestPromise(queryKey[0], {
  187. method: 'PUT',
  188. data: {
  189. metricsExtractionRules: filterTempIds(data.metricsExtractionRules),
  190. },
  191. });
  192. },
  193. {
  194. onMutate: createOptimisticUpdate(queryClient, queryKey, (variables, old) => {
  195. const updatedRules = variables.metricsExtractionRules;
  196. const updatedRulesMap = new Map(
  197. updatedRules.map(rule => [getRuleIdentifier(rule), rule])
  198. );
  199. return old?.map(rule => {
  200. const updatedRule = updatedRulesMap.get(getRuleIdentifier(rule));
  201. return updatedRule ?? rule;
  202. });
  203. }),
  204. onError: createRollback(queryClient, queryKey),
  205. onSettled: () => {
  206. queryClient.invalidateQueries(queryKey);
  207. queryClient.invalidateQueries(getMetricsExtractionOrgApiKey(orgSlug));
  208. },
  209. }
  210. );
  211. }