metrics.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import {useEffect, useMemo, useState} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import moment from 'moment';
  4. import {ApiResult} from 'sentry/api';
  5. import {
  6. DateTimeObject,
  7. getDiffInMinutes,
  8. getInterval,
  9. } from 'sentry/components/charts/utils';
  10. import {t} from 'sentry/locale';
  11. import {defined, formatBytesBase2, formatBytesBase10} from 'sentry/utils';
  12. import {formatPercentage, getDuration} from 'sentry/utils/formatters';
  13. import {ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import {DateString, PageFilters} from '../types/core';
  16. // TODO(ddm): reuse from types/metrics.tsx
  17. type MetricMeta = {
  18. mri: string;
  19. name: string;
  20. operations: string[];
  21. type: string;
  22. unit: string;
  23. };
  24. export enum MetricDisplayType {
  25. LINE = 'line',
  26. AREA = 'area',
  27. BAR = 'bar',
  28. TABLE = 'table',
  29. }
  30. export const defaultMetricDisplayType = MetricDisplayType.LINE;
  31. export function useMetricsMeta(
  32. projects: PageFilters['projects']
  33. ): Record<string, MetricMeta> {
  34. const {slug} = useOrganization();
  35. const getKey = (useCase: UseCase): ApiQueryKey => {
  36. return [
  37. `/organizations/${slug}/metrics/meta/`,
  38. {query: {useCase, project: projects}},
  39. ];
  40. };
  41. const opts = {
  42. staleTime: Infinity,
  43. };
  44. const {data: sessionsMeta = []} = useApiQuery<MetricMeta[]>(getKey('sessions'), opts);
  45. const {data: txnsMeta = []} = useApiQuery<MetricMeta[]>(getKey('transactions'), opts);
  46. const {data: customMeta = []} = useApiQuery<MetricMeta[]>(getKey('custom'), opts);
  47. return useMemo(
  48. () =>
  49. [...sessionsMeta, ...txnsMeta, ...customMeta].reduce((acc, metricMeta) => {
  50. return {...acc, [metricMeta.mri]: metricMeta};
  51. }, {}),
  52. [sessionsMeta, txnsMeta, customMeta]
  53. );
  54. }
  55. type MetricTag = {
  56. key: string;
  57. };
  58. export function useMetricsTags(mri: string, projects: PageFilters['projects']) {
  59. const {slug} = useOrganization();
  60. const useCase = getUseCaseFromMRI(mri);
  61. return useApiQuery<MetricTag[]>(
  62. [
  63. `/organizations/${slug}/metrics/tags/`,
  64. {query: {metric: mri, useCase, project: projects}},
  65. ],
  66. {
  67. staleTime: Infinity,
  68. }
  69. );
  70. }
  71. export function useMetricsTagValues(
  72. mri: string,
  73. tag: string,
  74. projects: PageFilters['projects']
  75. ) {
  76. const {slug} = useOrganization();
  77. const useCase = getUseCaseFromMRI(mri);
  78. return useApiQuery<MetricTag[]>(
  79. [
  80. `/organizations/${slug}/metrics/tags/${tag}/`,
  81. {query: {metric: mri, useCase, project: projects}},
  82. ],
  83. {
  84. staleTime: Infinity,
  85. enabled: !!tag,
  86. }
  87. );
  88. }
  89. export type MetricsQuery = {
  90. datetime: PageFilters['datetime'];
  91. environments: PageFilters['environments'];
  92. mri: string;
  93. projects: PageFilters['projects'];
  94. groupBy?: string[];
  95. op?: string;
  96. query?: string;
  97. };
  98. // TODO(ddm): reuse from types/metrics.tsx
  99. type Group = {
  100. by: Record<string, unknown>;
  101. series: Record<string, number[]>;
  102. totals: Record<string, number>;
  103. };
  104. // TODO(ddm): reuse from types/metrics.tsx
  105. export type MetricsData = {
  106. end: string;
  107. groups: Group[];
  108. intervals: string[];
  109. meta: MetricMeta[];
  110. query: string;
  111. start: string;
  112. };
  113. export function useMetricsData({
  114. mri,
  115. op,
  116. datetime,
  117. projects,
  118. environments,
  119. query,
  120. groupBy,
  121. }: MetricsQuery) {
  122. const {slug, features} = useOrganization();
  123. const useCase = getUseCaseFromMRI(mri);
  124. const field = op ? `${op}(${mri})` : mri;
  125. const interval = getMetricsInterval(datetime, mri);
  126. const queryToSend = {
  127. ...getDateTimeParams(datetime),
  128. query,
  129. project: projects,
  130. environment: environments,
  131. field,
  132. useCase,
  133. interval,
  134. groupBy,
  135. allowPrivate: true, // TODO(ddm): reconsider before widening audience
  136. // max result groups
  137. per_page: 20,
  138. useNewMetricsLayer: false,
  139. };
  140. if (features.includes('metrics-api-new-metrics-layer')) {
  141. queryToSend.useNewMetricsLayer = true;
  142. }
  143. return useApiQuery<MetricsData>(
  144. [`/organizations/${slug}/metrics/data/`, {query: queryToSend}],
  145. {
  146. retry: 0,
  147. staleTime: 0,
  148. refetchOnReconnect: true,
  149. refetchOnWindowFocus: true,
  150. refetchInterval: data => getRefetchInterval(data, interval),
  151. }
  152. );
  153. }
  154. function getRefetchInterval(
  155. data: ApiResult | undefined,
  156. interval: string
  157. ): number | false {
  158. // no data means request failed - don't refetch
  159. if (!data) {
  160. return false;
  161. }
  162. if (interval === '10s') {
  163. // refetch every 10 seconds
  164. return 10 * 1000;
  165. }
  166. // refetch every 60 seconds
  167. return 60 * 1000;
  168. }
  169. // Wraps useMetricsData and provides two additional features:
  170. // 1. return data is undefined only during the initial load
  171. // 2. provides a callback to trim the data to a specific time range when chart zoom is used
  172. export function useMetricsDataZoom(props: MetricsQuery) {
  173. const [metricsData, setMetricsData] = useState<MetricsData | undefined>();
  174. const {data: rawData, isLoading, isError, error} = useMetricsData(props);
  175. useEffect(() => {
  176. if (rawData) {
  177. setMetricsData(rawData);
  178. }
  179. }, [rawData]);
  180. const trimData = (start, end): MetricsData | undefined => {
  181. if (!metricsData) {
  182. return metricsData;
  183. }
  184. // find the index of the first interval that is greater than the start time
  185. const startIndex = metricsData.intervals.findIndex(interval => interval >= start) - 1;
  186. const endIndex = metricsData.intervals.findIndex(interval => interval >= end);
  187. if (startIndex === -1 || endIndex === -1) {
  188. return metricsData;
  189. }
  190. return {
  191. ...metricsData,
  192. intervals: metricsData.intervals.slice(startIndex, endIndex),
  193. groups: metricsData.groups.map(group => ({
  194. ...group,
  195. series: Object.fromEntries(
  196. Object.entries(group.series).map(([seriesName, series]) => [
  197. seriesName,
  198. series.slice(startIndex, endIndex),
  199. ])
  200. ),
  201. })),
  202. };
  203. };
  204. return {
  205. data: metricsData,
  206. isLoading,
  207. isError,
  208. error,
  209. onZoom: (start: DateString, end: DateString) => {
  210. setMetricsData(trimData(start, end));
  211. },
  212. };
  213. }
  214. // Wraps getInterval since other users of this function, and other metric use cases do not have support for 10s granularity
  215. function getMetricsInterval(dateTimeObj: DateTimeObject, mri: string) {
  216. const interval = getInterval(dateTimeObj, 'metrics');
  217. if (interval !== '1m') {
  218. return interval;
  219. }
  220. const diffInMinutes = getDiffInMinutes(dateTimeObj);
  221. const useCase = getUseCaseFromMRI(mri);
  222. if (diffInMinutes <= 60 && useCase === 'custom') {
  223. return '10s';
  224. }
  225. return interval;
  226. }
  227. function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
  228. return period
  229. ? {statsPeriod: period}
  230. : {start: moment(start).toISOString(), end: moment(end).toISOString()};
  231. }
  232. type UseCase = 'sessions' | 'transactions' | 'custom';
  233. const metricTypeToReadable = {
  234. c: t('counter'),
  235. g: t('gauge'),
  236. d: t('distribution'),
  237. s: t('set'),
  238. e: t('derived'),
  239. };
  240. // Converts from "c" to "counter"
  241. export function getReadableMetricType(type) {
  242. return metricTypeToReadable[type] ?? t('unknown');
  243. }
  244. const noUnit = 'none';
  245. export function parseMRI(mri?: string) {
  246. if (!mri) {
  247. return null;
  248. }
  249. const cleanMRI = mri.match(/[cdegs]:[\w/.@]+/)?.[0] ?? mri;
  250. const name = cleanMRI.match(/^[a-z]:\w+\/(.+)(?:@\w+)$/)?.[1] ?? mri;
  251. const unit = cleanMRI.split('@').pop() ?? noUnit;
  252. const useCase = getUseCaseFromMRI(cleanMRI);
  253. return {
  254. name,
  255. unit,
  256. mri: cleanMRI,
  257. useCase,
  258. };
  259. }
  260. export function getUseCaseFromMRI(mri?: string): UseCase {
  261. if (mri?.includes('custom/')) {
  262. return 'custom';
  263. }
  264. if (mri?.includes('transactions/')) {
  265. return 'transactions';
  266. }
  267. return 'sessions';
  268. }
  269. export function formatMetricUsingUnit(value: number | null, unit: string) {
  270. if (!defined(value)) {
  271. return '\u2014';
  272. }
  273. switch (unit) {
  274. case 'nanosecond':
  275. return getDuration(value / 1000000000, 2, true);
  276. case 'microsecond':
  277. return getDuration(value / 1000000, 2, true);
  278. case 'millisecond':
  279. return getDuration(value / 1000, 2, true);
  280. case 'second':
  281. return getDuration(value, 2, true);
  282. case 'minute':
  283. return getDuration(value * 60, 2, true);
  284. case 'hour':
  285. return getDuration(value * 60 * 60, 2, true);
  286. case 'day':
  287. return getDuration(value * 60 * 60 * 24, 2, true);
  288. case 'week':
  289. return getDuration(value * 60 * 60 * 24 * 7, 2, true);
  290. case 'ratio':
  291. return formatPercentage(value, 2);
  292. case 'percent':
  293. return formatPercentage(value / 100, 2);
  294. case 'bit':
  295. return formatBytesBase2(value / 8);
  296. case 'byte':
  297. return formatBytesBase10(value);
  298. case 'kibibyte':
  299. return formatBytesBase2(value * 1024);
  300. case 'kilobyte':
  301. return formatBytesBase10(value, 1);
  302. case 'mebibyte':
  303. return formatBytesBase2(value * 1024 ** 2);
  304. case 'megabyte':
  305. return formatBytesBase10(value, 2);
  306. case 'gibibyte':
  307. return formatBytesBase2(value * 1024 ** 3);
  308. case 'gigabyte':
  309. return formatBytesBase10(value, 3);
  310. case 'tebibyte':
  311. return formatBytesBase2(value * 1024 ** 4);
  312. case 'terabyte':
  313. return formatBytesBase10(value, 4);
  314. case 'pebibyte':
  315. return formatBytesBase2(value * 1024 ** 5);
  316. case 'petabyte':
  317. return formatBytesBase10(value, 5);
  318. case 'exbibyte':
  319. return formatBytesBase2(value * 1024 ** 6);
  320. case 'exabyte':
  321. return formatBytesBase10(value, 6);
  322. case 'none':
  323. default:
  324. return value.toLocaleString();
  325. }
  326. }
  327. export function formatMetricsUsingUnitAndOp(
  328. value: number | null,
  329. unit: string,
  330. operation?: string
  331. ) {
  332. if (operation === 'count') {
  333. // if the operation is count, we want to ignore the unit and always format the value as a number
  334. return value?.toLocaleString() ?? '';
  335. }
  336. return formatMetricUsingUnit(value, unit);
  337. }
  338. export function isAllowedOp(op: string) {
  339. return !['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
  340. }
  341. export function updateQuery(router: InjectedRouter, partialQuery: Record<string, any>) {
  342. router.push({
  343. ...router.location,
  344. query: {
  345. ...router.location.query,
  346. ...partialQuery,
  347. },
  348. });
  349. }
  350. export function clearQuery(router: InjectedRouter) {
  351. router.push({
  352. ...router.location,
  353. query: {},
  354. });
  355. }