index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import {InjectedRouter} from 'react-router';
  2. import round from 'lodash/round';
  3. import moment from 'moment';
  4. import * as qs from 'query-string';
  5. import {
  6. DateTimeObject,
  7. getDiffInMinutes,
  8. getInterval,
  9. } from 'sentry/components/charts/utils';
  10. import {t} from 'sentry/locale';
  11. import {MetricsApiResponse} from 'sentry/types';
  12. import {
  13. MetricMeta,
  14. MetricsApiRequestMetric,
  15. MetricsApiRequestQuery,
  16. MetricsGroup,
  17. MetricType,
  18. MRI,
  19. UseCase,
  20. } from 'sentry/types/metrics';
  21. import {defined, formatBytesBase2, formatBytesBase10} from 'sentry/utils';
  22. import {formatPercentage, getDuration} from 'sentry/utils/formatters';
  23. import {formatMRI, getUseCaseFromMRI, parseField} from 'sentry/utils/metrics/mri';
  24. import {DateString, PageFilters} from '../../types/core';
  25. export enum MetricDisplayType {
  26. LINE = 'line',
  27. AREA = 'area',
  28. BAR = 'bar',
  29. TABLE = 'table',
  30. }
  31. export const defaultMetricDisplayType = MetricDisplayType.LINE;
  32. export const getMetricDisplayType = (displayType: unknown): MetricDisplayType => {
  33. if (
  34. [
  35. MetricDisplayType.AREA,
  36. MetricDisplayType.BAR,
  37. MetricDisplayType.LINE,
  38. MetricDisplayType.TABLE,
  39. ].includes(displayType as MetricDisplayType)
  40. ) {
  41. return displayType as MetricDisplayType;
  42. }
  43. return MetricDisplayType.LINE;
  44. };
  45. export type MetricTag = {
  46. key: string;
  47. };
  48. export type SortState = {
  49. name: 'name' | 'avg' | 'min' | 'max' | 'sum' | undefined;
  50. order: 'asc' | 'desc';
  51. };
  52. export interface MetricWidgetQueryParams
  53. extends Pick<MetricsQuery, 'mri' | 'op' | 'query' | 'groupBy'> {
  54. displayType: MetricDisplayType;
  55. focusedSeries?: string;
  56. position?: number;
  57. powerUserMode?: boolean;
  58. showSummaryTable?: boolean;
  59. sort?: SortState;
  60. }
  61. export interface DdmQueryParams {
  62. widgets: string; // stringified json representation of MetricWidgetQueryParams
  63. end?: DateString;
  64. environment?: string[];
  65. project?: number[];
  66. start?: DateString;
  67. statsPeriod?: string | null;
  68. utc?: boolean | null;
  69. }
  70. export type MetricsQuery = {
  71. datetime: PageFilters['datetime'];
  72. environments: PageFilters['environments'];
  73. mri: MRI;
  74. projects: PageFilters['projects'];
  75. groupBy?: string[];
  76. op?: string;
  77. query?: string;
  78. };
  79. export type MetricMetaCodeLocation = {
  80. frames: {
  81. absPath?: string;
  82. filename?: string;
  83. function?: string;
  84. lineNo?: number;
  85. module?: string;
  86. }[];
  87. mri: string;
  88. timestamp: number;
  89. };
  90. export function getDdmUrl(
  91. orgSlug: string,
  92. {
  93. widgets,
  94. start,
  95. end,
  96. statsPeriod,
  97. project,
  98. ...otherParams
  99. }: Omit<DdmQueryParams, 'project' | 'widgets'> & {
  100. widgets: MetricWidgetQueryParams[];
  101. project?: (string | number)[];
  102. }
  103. ) {
  104. const urlParams: Partial<DdmQueryParams> = {
  105. ...otherParams,
  106. project: project?.map(id => (typeof id === 'string' ? parseInt(id, 10) : id)),
  107. widgets: JSON.stringify(widgets),
  108. };
  109. if (statsPeriod) {
  110. urlParams.statsPeriod = statsPeriod;
  111. } else {
  112. urlParams.start = start;
  113. urlParams.end = end;
  114. }
  115. return `/organizations/${orgSlug}/ddm/?${qs.stringify(urlParams)}`;
  116. }
  117. export function getMetricsApiRequestQuery(
  118. {field, query, groupBy}: MetricsApiRequestMetric,
  119. {projects, environments, datetime}: PageFilters,
  120. overrides: Partial<MetricsApiRequestQuery>
  121. ): MetricsApiRequestQuery {
  122. const {mri: mri} = parseField(field) ?? {};
  123. const useCase = getUseCaseFromMRI(mri) ?? 'custom';
  124. const interval = getMetricsInterval(datetime, useCase);
  125. const queryToSend = {
  126. ...getDateTimeParams(datetime),
  127. query,
  128. project: projects,
  129. environment: environments,
  130. field,
  131. useCase,
  132. interval,
  133. groupBy,
  134. allowPrivate: true, // TODO(ddm): reconsider before widening audience
  135. // max result groups
  136. per_page: 10,
  137. };
  138. return {...queryToSend, ...overrides};
  139. }
  140. // Wraps getInterval since other users of this function, and other metric use cases do not have support for 10s granularity
  141. export function getMetricsInterval(dateTimeObj: DateTimeObject, useCase: UseCase) {
  142. const interval = getInterval(dateTimeObj, 'metrics');
  143. if (interval !== '1m') {
  144. return interval;
  145. }
  146. const diffInMinutes = getDiffInMinutes(dateTimeObj);
  147. if (diffInMinutes <= 60 && useCase === 'custom') {
  148. return '10s';
  149. }
  150. return interval;
  151. }
  152. export function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
  153. return period
  154. ? {statsPeriod: period}
  155. : {start: moment(start).toISOString(), end: moment(end).toISOString()};
  156. }
  157. const metricTypeToReadable: Record<MetricType, string> = {
  158. c: t('counter'),
  159. g: t('gauge'),
  160. d: t('distribution'),
  161. s: t('set'),
  162. e: t('derived'),
  163. };
  164. // Converts from "c" to "counter"
  165. export function getReadableMetricType(type?: string) {
  166. return metricTypeToReadable[type as MetricType] ?? t('unknown');
  167. }
  168. // The metric units that we have support for in the UI
  169. // others will still be displayed, but will not have any effect on formatting
  170. export const formattingSupportedMetricUnits = [
  171. 'none',
  172. 'nanosecond',
  173. 'microsecond',
  174. 'millisecond',
  175. 'second',
  176. 'minute',
  177. 'hour',
  178. 'day',
  179. 'week',
  180. 'ratio',
  181. 'percent',
  182. 'bit',
  183. 'byte',
  184. 'kibibyte',
  185. 'kilobyte',
  186. 'mebibyte',
  187. 'megabyte',
  188. 'gibibyte',
  189. 'gigabyte',
  190. 'tebibyte',
  191. 'terabyte',
  192. 'pebibyte',
  193. 'petabyte',
  194. 'exbibyte',
  195. 'exabyte',
  196. ] as const;
  197. type FormattingSupportedMetricUnit = (typeof formattingSupportedMetricUnits)[number];
  198. export function formatMetricUsingUnit(value: number | null, unit: string) {
  199. if (!defined(value)) {
  200. return '\u2014';
  201. }
  202. switch (unit as FormattingSupportedMetricUnit) {
  203. case 'nanosecond':
  204. return getDuration(value / 1000000000, 2, true);
  205. case 'microsecond':
  206. return getDuration(value / 1000000, 2, true);
  207. case 'millisecond':
  208. return getDuration(value / 1000, 2, true);
  209. case 'second':
  210. return getDuration(value, 2, true);
  211. case 'minute':
  212. return getDuration(value * 60, 2, true);
  213. case 'hour':
  214. return getDuration(value * 60 * 60, 2, true);
  215. case 'day':
  216. return getDuration(value * 60 * 60 * 24, 2, true);
  217. case 'week':
  218. return getDuration(value * 60 * 60 * 24 * 7, 2, true);
  219. case 'ratio':
  220. return formatPercentage(value, 2);
  221. case 'percent':
  222. return formatPercentage(value / 100, 2);
  223. case 'bit':
  224. return formatBytesBase2(value / 8);
  225. case 'byte':
  226. return formatBytesBase10(value);
  227. case 'kibibyte':
  228. return formatBytesBase2(value * 1024);
  229. case 'kilobyte':
  230. return formatBytesBase10(value, 1);
  231. case 'mebibyte':
  232. return formatBytesBase2(value * 1024 ** 2);
  233. case 'megabyte':
  234. return formatBytesBase10(value, 2);
  235. case 'gibibyte':
  236. return formatBytesBase2(value * 1024 ** 3);
  237. case 'gigabyte':
  238. return formatBytesBase10(value, 3);
  239. case 'tebibyte':
  240. return formatBytesBase2(value * 1024 ** 4);
  241. case 'terabyte':
  242. return formatBytesBase10(value, 4);
  243. case 'pebibyte':
  244. return formatBytesBase2(value * 1024 ** 5);
  245. case 'petabyte':
  246. return formatBytesBase10(value, 5);
  247. case 'exbibyte':
  248. return formatBytesBase2(value * 1024 ** 6);
  249. case 'exabyte':
  250. return formatBytesBase10(value, 6);
  251. case 'none':
  252. default:
  253. return value.toLocaleString();
  254. }
  255. }
  256. const METRIC_UNIT_TO_SHORT: Record<FormattingSupportedMetricUnit, string> = {
  257. nanosecond: 'ns',
  258. microsecond: 'μs',
  259. millisecond: 'ms',
  260. second: 's',
  261. minute: 'min',
  262. hour: 'hr',
  263. day: 'day',
  264. week: 'wk',
  265. ratio: '%',
  266. percent: '%',
  267. bit: 'b',
  268. byte: 'B',
  269. kibibyte: 'KiB',
  270. kilobyte: 'KB',
  271. mebibyte: 'MiB',
  272. megabyte: 'MB',
  273. gibibyte: 'GiB',
  274. gigabyte: 'GB',
  275. tebibyte: 'TiB',
  276. terabyte: 'TB',
  277. pebibyte: 'PiB',
  278. petabyte: 'PB',
  279. exbibyte: 'EiB',
  280. exabyte: 'EB',
  281. none: '',
  282. };
  283. const getShortMetricUnit = (unit: string): string => METRIC_UNIT_TO_SHORT[unit] ?? '';
  284. export function formatMetricUsingFixedUnit(
  285. value: number | null,
  286. unit: string,
  287. op?: string
  288. ) {
  289. if (value === null) {
  290. return '\u2014';
  291. }
  292. return op === 'count'
  293. ? round(value, 3).toLocaleString()
  294. : `${round(value, 3).toLocaleString()}${getShortMetricUnit(unit)}`.trim();
  295. }
  296. export function formatMetricsUsingUnitAndOp(
  297. value: number | null,
  298. unit: string,
  299. operation?: string
  300. ) {
  301. if (operation === 'count') {
  302. // if the operation is count, we want to ignore the unit and always format the value as a number
  303. return value?.toLocaleString() ?? '';
  304. }
  305. return formatMetricUsingUnit(value, unit);
  306. }
  307. export function isAllowedOp(op: string) {
  308. return !['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
  309. }
  310. export function updateQuery(router: InjectedRouter, partialQuery: Record<string, any>) {
  311. router.push({
  312. ...router.location,
  313. query: {
  314. ...router.location.query,
  315. ...partialQuery,
  316. },
  317. });
  318. }
  319. export function clearQuery(router: InjectedRouter) {
  320. router.push({
  321. ...router.location,
  322. query: {},
  323. });
  324. }
  325. // TODO(ddm): there has to be a nicer way to do this
  326. export function getSeriesName(
  327. group: MetricsGroup,
  328. isOnlyGroup = false,
  329. groupBy: MetricsQuery['groupBy']
  330. ) {
  331. if (isOnlyGroup && !groupBy?.length) {
  332. const field = Object.keys(group.series)?.[0];
  333. const {mri} = parseField(field) ?? {mri: field};
  334. const name = formatMRI(mri as MRI);
  335. return name ?? '(none)';
  336. }
  337. return Object.entries(group.by)
  338. .map(([key, value]) => `${key}:${String(value).length ? value : t('none')}`)
  339. .join(', ');
  340. }
  341. export function groupByOp(metrics: MetricMeta[]): Record<string, MetricMeta[]> {
  342. const uniqueOperations = [
  343. ...new Set(metrics.flatMap(field => field.operations).filter(isAllowedOp)),
  344. ].sort();
  345. const groupedByOp = uniqueOperations.reduce((result, op) => {
  346. result[op] = metrics.filter(field => field.operations.includes(op));
  347. return result;
  348. }, {});
  349. return groupedByOp;
  350. }
  351. // TODO(ddm): remove this and all of its usages once backend sends mri fields
  352. export function mapToMRIFields(
  353. data: MetricsApiResponse | undefined,
  354. fields: string[]
  355. ): void {
  356. if (!data) {
  357. return;
  358. }
  359. data.groups.forEach(group => {
  360. group.series = swapObjectKeys(group.series, fields);
  361. group.totals = swapObjectKeys(group.totals, fields);
  362. });
  363. }
  364. function swapObjectKeys(obj: Record<string, unknown> | undefined, newKeys: string[]) {
  365. if (!obj) {
  366. return {};
  367. }
  368. return Object.keys(obj).reduce((acc, key, index) => {
  369. acc[newKeys[index]] = obj[key];
  370. return acc;
  371. }, {});
  372. }