index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import {InjectedRouter} from 'react-router';
  2. import moment from 'moment';
  3. import * as qs from 'query-string';
  4. import {
  5. DateTimeObject,
  6. getDiffInMinutes,
  7. getInterval,
  8. } from 'sentry/components/charts/utils';
  9. import {t} from 'sentry/locale';
  10. import {
  11. MetricMeta,
  12. MetricsApiRequestMetric,
  13. MetricsApiRequestQuery,
  14. MetricsGroup,
  15. MetricType,
  16. MRI,
  17. UseCase,
  18. } from 'sentry/types/metrics';
  19. import {defined, formatBytesBase2, formatBytesBase10} from 'sentry/utils';
  20. import {formatPercentage, getDuration} from 'sentry/utils/formatters';
  21. import {formatMRI, getUseCaseFromMRI, parseField} from 'sentry/utils/metrics/mri';
  22. import {DateString, PageFilters} from '../../types/core';
  23. export enum MetricDisplayType {
  24. LINE = 'line',
  25. AREA = 'area',
  26. BAR = 'bar',
  27. TABLE = 'table',
  28. }
  29. export const defaultMetricDisplayType = MetricDisplayType.LINE;
  30. export type MetricTag = {
  31. key: string;
  32. };
  33. export type SortState = {
  34. name: 'name' | 'avg' | 'min' | 'max' | 'sum' | undefined;
  35. order: 'asc' | 'desc';
  36. };
  37. export interface MetricWidgetQueryParams
  38. extends Pick<MetricsQuery, 'mri' | 'op' | 'query' | 'groupBy'> {
  39. displayType: MetricDisplayType;
  40. focusedSeries?: string;
  41. position?: number;
  42. powerUserMode?: boolean;
  43. showSummaryTable?: boolean;
  44. sort?: SortState;
  45. }
  46. export interface DdmQueryParams {
  47. widgets: string; // stringified json representation of MetricWidgetQueryParams
  48. end?: DateString;
  49. environment?: string[];
  50. project?: number[];
  51. start?: DateString;
  52. statsPeriod?: string | null;
  53. utc?: boolean | null;
  54. }
  55. export type MetricsQuery = {
  56. datetime: PageFilters['datetime'];
  57. environments: PageFilters['environments'];
  58. mri: MRI;
  59. projects: PageFilters['projects'];
  60. groupBy?: string[];
  61. op?: string;
  62. query?: string;
  63. };
  64. export type MetricMetaCodeLocation = {
  65. frames: {
  66. absPath?: string;
  67. filename?: string;
  68. function?: string;
  69. lineNo?: number;
  70. module?: string;
  71. }[];
  72. mri: string;
  73. timestamp: number;
  74. };
  75. export function getDdmUrl(
  76. orgSlug: string,
  77. {
  78. widgets,
  79. start,
  80. end,
  81. statsPeriod,
  82. project,
  83. ...otherParams
  84. }: Omit<DdmQueryParams, 'project' | 'widgets'> & {
  85. widgets: MetricWidgetQueryParams[];
  86. project?: (string | number)[];
  87. }
  88. ) {
  89. const urlParams: Partial<DdmQueryParams> = {
  90. ...otherParams,
  91. project: project?.map(id => (typeof id === 'string' ? parseInt(id, 10) : id)),
  92. widgets: JSON.stringify(widgets),
  93. };
  94. if (statsPeriod) {
  95. urlParams.statsPeriod = statsPeriod;
  96. } else {
  97. urlParams.start = start;
  98. urlParams.end = end;
  99. }
  100. return `/organizations/${orgSlug}/ddm/?${qs.stringify(urlParams)}`;
  101. }
  102. export function getMetricsApiRequestQuery(
  103. {field, query, groupBy}: MetricsApiRequestMetric,
  104. {projects, environments, datetime}: PageFilters,
  105. overrides: Partial<MetricsApiRequestQuery>
  106. ): MetricsApiRequestQuery {
  107. const {mri: mri} = parseField(field) ?? {};
  108. const useCase = getUseCaseFromMRI(mri) ?? 'custom';
  109. const interval = getMetricsInterval(datetime, useCase);
  110. const queryToSend = {
  111. ...getDateTimeParams(datetime),
  112. query,
  113. project: projects,
  114. environment: environments,
  115. field,
  116. useCase,
  117. interval,
  118. groupBy,
  119. allowPrivate: true, // TODO(ddm): reconsider before widening audience
  120. // max result groups
  121. per_page: 10,
  122. };
  123. return {...queryToSend, ...overrides};
  124. }
  125. // Wraps getInterval since other users of this function, and other metric use cases do not have support for 10s granularity
  126. export function getMetricsInterval(dateTimeObj: DateTimeObject, useCase: UseCase) {
  127. const interval = getInterval(dateTimeObj, 'metrics');
  128. if (interval !== '1m') {
  129. return interval;
  130. }
  131. const diffInMinutes = getDiffInMinutes(dateTimeObj);
  132. if (diffInMinutes <= 60 && useCase === 'custom') {
  133. return '10s';
  134. }
  135. return interval;
  136. }
  137. export function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
  138. return period
  139. ? {statsPeriod: period}
  140. : {start: moment(start).toISOString(), end: moment(end).toISOString()};
  141. }
  142. const metricTypeToReadable: Record<MetricType, string> = {
  143. c: t('counter'),
  144. g: t('gauge'),
  145. d: t('distribution'),
  146. s: t('set'),
  147. e: t('derived'),
  148. };
  149. // Converts from "c" to "counter"
  150. export function getReadableMetricType(type?: string) {
  151. return metricTypeToReadable[type as MetricType] ?? t('unknown');
  152. }
  153. export function formatMetricUsingUnit(value: number | null, unit: string) {
  154. if (!defined(value)) {
  155. return '\u2014';
  156. }
  157. switch (unit) {
  158. case 'nanosecond':
  159. return getDuration(value / 1000000000, 2, true);
  160. case 'microsecond':
  161. return getDuration(value / 1000000, 2, true);
  162. case 'millisecond':
  163. return getDuration(value / 1000, 2, true);
  164. case 'second':
  165. return getDuration(value, 2, true);
  166. case 'minute':
  167. return getDuration(value * 60, 2, true);
  168. case 'hour':
  169. return getDuration(value * 60 * 60, 2, true);
  170. case 'day':
  171. return getDuration(value * 60 * 60 * 24, 2, true);
  172. case 'week':
  173. return getDuration(value * 60 * 60 * 24 * 7, 2, true);
  174. case 'ratio':
  175. return formatPercentage(value, 2);
  176. case 'percent':
  177. return formatPercentage(value / 100, 2);
  178. case 'bit':
  179. return formatBytesBase2(value / 8);
  180. case 'byte':
  181. return formatBytesBase10(value);
  182. case 'kibibyte':
  183. return formatBytesBase2(value * 1024);
  184. case 'kilobyte':
  185. return formatBytesBase10(value, 1);
  186. case 'mebibyte':
  187. return formatBytesBase2(value * 1024 ** 2);
  188. case 'megabyte':
  189. return formatBytesBase10(value, 2);
  190. case 'gibibyte':
  191. return formatBytesBase2(value * 1024 ** 3);
  192. case 'gigabyte':
  193. return formatBytesBase10(value, 3);
  194. case 'tebibyte':
  195. return formatBytesBase2(value * 1024 ** 4);
  196. case 'terabyte':
  197. return formatBytesBase10(value, 4);
  198. case 'pebibyte':
  199. return formatBytesBase2(value * 1024 ** 5);
  200. case 'petabyte':
  201. return formatBytesBase10(value, 5);
  202. case 'exbibyte':
  203. return formatBytesBase2(value * 1024 ** 6);
  204. case 'exabyte':
  205. return formatBytesBase10(value, 6);
  206. case 'none':
  207. default:
  208. return value.toLocaleString();
  209. }
  210. }
  211. export function formatMetricsUsingUnitAndOp(
  212. value: number | null,
  213. unit: string,
  214. operation?: string
  215. ) {
  216. if (operation === 'count') {
  217. // if the operation is count, we want to ignore the unit and always format the value as a number
  218. return value?.toLocaleString() ?? '';
  219. }
  220. return formatMetricUsingUnit(value, unit);
  221. }
  222. export function isAllowedOp(op: string) {
  223. return !['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
  224. }
  225. export function updateQuery(router: InjectedRouter, partialQuery: Record<string, any>) {
  226. router.push({
  227. ...router.location,
  228. query: {
  229. ...router.location.query,
  230. ...partialQuery,
  231. },
  232. });
  233. }
  234. export function clearQuery(router: InjectedRouter) {
  235. router.push({
  236. ...router.location,
  237. query: {},
  238. });
  239. }
  240. // TODO(ddm): there has to be a nicer way to do this
  241. export function getSeriesName(
  242. group: MetricsGroup,
  243. isOnlyGroup = false,
  244. groupBy: MetricsQuery['groupBy']
  245. ) {
  246. if (isOnlyGroup && !groupBy?.length) {
  247. const field = Object.keys(group.series)?.[0];
  248. const {mri} = parseField(field) ?? {mri: field};
  249. const name = formatMRI(mri as MRI);
  250. return name ?? '(none)';
  251. }
  252. return Object.entries(group.by)
  253. .map(([key, value]) => `${key}:${String(value).length ? value : t('none')}`)
  254. .join(', ');
  255. }
  256. export function groupByOp(metrics: MetricMeta[]): Record<string, MetricMeta[]> {
  257. const uniqueOperations = [
  258. ...new Set(metrics.flatMap(field => field.operations).filter(isAllowedOp)),
  259. ].sort();
  260. const groupedByOp = uniqueOperations.reduce((result, op) => {
  261. result[op] = metrics.filter(field => field.operations.includes(op));
  262. return result;
  263. }, {});
  264. return groupedByOp;
  265. }