metrics.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import {useMemo} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import moment from 'moment';
  4. import {getInterval} from 'sentry/components/charts/utils';
  5. import {t} from 'sentry/locale';
  6. import {defined, formatBytesBase2, formatBytesBase10} from 'sentry/utils';
  7. import {formatPercentage, getDuration} from 'sentry/utils/formatters';
  8. import {ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient';
  9. import useOrganization from 'sentry/utils/useOrganization';
  10. import {PageFilters} from '../types/core';
  11. // TODO(ddm): reuse from types/metrics.tsx
  12. type MetricMeta = {
  13. mri: string;
  14. name: string;
  15. operations: string[];
  16. type: string;
  17. unit: string;
  18. };
  19. export enum MetricDisplayType {
  20. LINE = 'line',
  21. AREA = 'area',
  22. BAR = 'bar',
  23. TABLE = 'table',
  24. }
  25. export const defaultMetricDisplayType = MetricDisplayType.LINE;
  26. export function useMetricsMeta(
  27. projects: PageFilters['projects']
  28. ): Record<string, MetricMeta> {
  29. const {slug} = useOrganization();
  30. const getKey = (useCase: UseCase): ApiQueryKey => {
  31. return [
  32. `/organizations/${slug}/metrics/meta/`,
  33. {query: {useCase, project: projects}},
  34. ];
  35. };
  36. const opts = {
  37. staleTime: Infinity,
  38. };
  39. const {data: sessionsMeta = []} = useApiQuery<MetricMeta[]>(getKey('sessions'), opts);
  40. const {data: txnsMeta = []} = useApiQuery<MetricMeta[]>(getKey('transactions'), opts);
  41. const {data: customMeta = []} = useApiQuery<MetricMeta[]>(getKey('custom'), opts);
  42. return useMemo(
  43. () =>
  44. [...sessionsMeta, ...txnsMeta, ...customMeta].reduce((acc, metricMeta) => {
  45. return {...acc, [metricMeta.mri]: metricMeta};
  46. }, {}),
  47. [sessionsMeta, txnsMeta, customMeta]
  48. );
  49. }
  50. type MetricTag = {
  51. key: string;
  52. };
  53. export function useMetricsTags(mri: string, projects: PageFilters['projects']) {
  54. const {slug} = useOrganization();
  55. const useCase = getUseCaseFromMri(mri);
  56. return useApiQuery<MetricTag[]>(
  57. [
  58. `/organizations/${slug}/metrics/tags/`,
  59. {query: {metric: mri, useCase, project: projects}},
  60. ],
  61. {
  62. staleTime: Infinity,
  63. }
  64. );
  65. }
  66. export function useMetricsTagValues(
  67. mri: string,
  68. tag: string,
  69. projects: PageFilters['projects']
  70. ) {
  71. const {slug} = useOrganization();
  72. const useCase = getUseCaseFromMri(mri);
  73. return useApiQuery<MetricTag[]>(
  74. [
  75. `/organizations/${slug}/metrics/tags/${tag}/`,
  76. {query: {metric: mri, useCase, project: projects}},
  77. ],
  78. {
  79. staleTime: Infinity,
  80. enabled: !!tag,
  81. }
  82. );
  83. }
  84. export type MetricsQuery = {
  85. mri: string;
  86. groupBy?: string[];
  87. op?: string;
  88. query?: string;
  89. };
  90. export type MetricsDataProps = MetricsQuery & {
  91. datetime: PageFilters['datetime'];
  92. environments: PageFilters['environments'];
  93. projects: PageFilters['projects'];
  94. };
  95. // TODO(ddm): reuse from types/metrics.tsx
  96. type Group = {
  97. by: Record<string, unknown>;
  98. series: Record<string, number[]>;
  99. totals: Record<string, number>;
  100. };
  101. // TODO(ddm): reuse from types/metrics.tsx
  102. export type MetricsData = {
  103. end: string;
  104. groups: Group[];
  105. intervals: string[];
  106. meta: MetricMeta[];
  107. query: string;
  108. start: string;
  109. };
  110. export function useMetricsData({
  111. mri,
  112. op,
  113. datetime,
  114. projects,
  115. environments,
  116. query,
  117. groupBy,
  118. }: MetricsDataProps) {
  119. const {slug} = useOrganization();
  120. const useCase = getUseCaseFromMri(mri);
  121. const field = op ? `${op}(${mri})` : mri;
  122. const interval = getInterval(datetime, 'metrics');
  123. const queryToSend = {
  124. ...getDateTimeParams(datetime),
  125. query,
  126. project: projects,
  127. environment: environments,
  128. field,
  129. useCase,
  130. interval,
  131. groupBy,
  132. allowPrivate: true, // TODO(ddm): reconsider before widening audience
  133. // max result groups
  134. per_page: 20,
  135. };
  136. return useApiQuery<MetricsData>(
  137. [`/organizations/${slug}/metrics/data/`, {query: queryToSend}],
  138. {
  139. retry: 0,
  140. staleTime: 0,
  141. refetchOnReconnect: true,
  142. refetchOnWindowFocus: true,
  143. // auto refetch every 60 seconds
  144. refetchInterval: data => {
  145. // don't refetch if the request failed
  146. if (!data) {
  147. return false;
  148. }
  149. return 60 * 1000;
  150. },
  151. }
  152. );
  153. }
  154. function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
  155. return period
  156. ? {statsPeriod: period}
  157. : {start: moment(start).toISOString(), end: moment(end).toISOString()};
  158. }
  159. type UseCase = 'sessions' | 'transactions' | 'custom';
  160. export function getUseCaseFromMri(mri?: string): UseCase {
  161. if (mri?.includes('custom/')) {
  162. return 'custom';
  163. }
  164. if (mri?.includes('transactions/')) {
  165. return 'transactions';
  166. }
  167. return 'sessions';
  168. }
  169. const metricTypeToReadable = {
  170. c: t('counter'),
  171. g: t('gauge'),
  172. d: t('distribution'),
  173. s: t('set'),
  174. e: t('derived'),
  175. };
  176. // Converts from "c" to "counter"
  177. export function getReadableMetricType(type) {
  178. return metricTypeToReadable[type] ?? t('unknown');
  179. }
  180. const noUnit = 'none';
  181. export function getUnitFromMRI(mri?: string) {
  182. if (!mri) {
  183. return noUnit;
  184. }
  185. return mri.split('@').pop() ?? noUnit;
  186. }
  187. export function getNameFromMRI(mri: string) {
  188. return mri.match(/^[a-z]:\w+\/(.+)(?:@\w+)$/)?.[1] ?? mri;
  189. }
  190. export function formatMetricUsingUnit(value: number | null, unit: string) {
  191. if (!defined(value)) {
  192. return '\u2014';
  193. }
  194. switch (unit) {
  195. case 'nanosecond':
  196. return getDuration(value / 1000000000, 2, true);
  197. case 'microsecond':
  198. return getDuration(value / 1000000, 2, true);
  199. case 'millisecond':
  200. return getDuration(value / 1000, 2, true);
  201. case 'second':
  202. return getDuration(value, 2, true);
  203. case 'minute':
  204. return getDuration(value * 60, 2, true);
  205. case 'hour':
  206. return getDuration(value * 60 * 60, 2, true);
  207. case 'day':
  208. return getDuration(value * 60 * 60 * 24, 2, true);
  209. case 'week':
  210. return getDuration(value * 60 * 60 * 24 * 7, 2, true);
  211. case 'ratio':
  212. return formatPercentage(value, 2);
  213. case 'percent':
  214. return formatPercentage(value / 100, 2);
  215. case 'bit':
  216. return formatBytesBase2(value / 8);
  217. case 'byte':
  218. return formatBytesBase10(value);
  219. case 'kibibyte':
  220. return formatBytesBase2(value * 1024);
  221. case 'kilobyte':
  222. return formatBytesBase10(value, 1);
  223. case 'mebibyte':
  224. return formatBytesBase2(value * 1024 ** 2);
  225. case 'megabyte':
  226. return formatBytesBase10(value, 2);
  227. case 'gibibyte':
  228. return formatBytesBase2(value * 1024 ** 3);
  229. case 'gigabyte':
  230. return formatBytesBase10(value, 3);
  231. case 'tebibyte':
  232. return formatBytesBase2(value * 1024 ** 4);
  233. case 'terabyte':
  234. return formatBytesBase10(value, 4);
  235. case 'pebibyte':
  236. return formatBytesBase2(value * 1024 ** 5);
  237. case 'petabyte':
  238. return formatBytesBase10(value, 5);
  239. case 'exbibyte':
  240. return formatBytesBase2(value * 1024 ** 6);
  241. case 'exabyte':
  242. return formatBytesBase10(value, 6);
  243. case 'none':
  244. default:
  245. return value.toLocaleString();
  246. }
  247. }
  248. export function formatMetricsUsingUnitAndOp(
  249. value: number | null,
  250. unit: string,
  251. operation?: string
  252. ) {
  253. if (operation === 'count') {
  254. // if the operation is count, we want to ignore the unit and always format the value as a number
  255. return value?.toLocaleString() ?? '';
  256. }
  257. return formatMetricUsingUnit(value, unit);
  258. }
  259. export function isAllowedOp(op: string) {
  260. return !['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
  261. }
  262. export function updateQuery(router: InjectedRouter, partialQuery: Record<string, any>) {
  263. router.push({
  264. ...router.location,
  265. query: {
  266. ...router.location.query,
  267. ...partialQuery,
  268. },
  269. });
  270. }