index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import {useCallback, useRef} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import moment from 'moment';
  4. import * as qs from 'query-string';
  5. import {
  6. DateTimeObject,
  7. Fidelity,
  8. getDiffInMinutes,
  9. GranularityLadder,
  10. ONE_HOUR,
  11. ONE_WEEK,
  12. SIX_HOURS,
  13. SIXTY_DAYS,
  14. THIRTY_DAYS,
  15. TWENTY_FOUR_HOURS,
  16. TWO_WEEKS,
  17. } from 'sentry/components/charts/utils';
  18. import {
  19. normalizeDateTimeParams,
  20. parseStatsPeriod,
  21. } from 'sentry/components/organizations/pageFilters/parse';
  22. import {t} from 'sentry/locale';
  23. import {MetricsApiResponse, PageFilters} from 'sentry/types';
  24. import type {
  25. MetricMeta,
  26. MetricsApiRequestMetric,
  27. MetricsApiRequestQuery,
  28. MetricsApiRequestQueryOptions,
  29. MetricsGroup,
  30. MetricsOperation,
  31. MRI,
  32. UseCase,
  33. } from 'sentry/types/metrics';
  34. import {isMeasurement as isMeasurementName} from 'sentry/utils/discover/fields';
  35. import {getMeasurements} from 'sentry/utils/measurements/measurements';
  36. import {
  37. formatMRI,
  38. formatMRIField,
  39. getUseCaseFromMRI,
  40. MRIToField,
  41. parseField,
  42. parseMRI,
  43. } from 'sentry/utils/metrics/mri';
  44. import type {
  45. DdmQueryParams,
  46. MetricsQuery,
  47. MetricsQuerySubject,
  48. MetricWidgetQueryParams,
  49. } from 'sentry/utils/metrics/types';
  50. import {MetricDisplayType} from 'sentry/utils/metrics/types';
  51. import useRouter from 'sentry/utils/useRouter';
  52. export function getDefaultMetricDisplayType(
  53. mri: MetricsQuery['mri'],
  54. op: MetricsQuery['op']
  55. ): MetricDisplayType {
  56. if (mri?.startsWith('c') || op === 'count') {
  57. return MetricDisplayType.BAR;
  58. }
  59. return MetricDisplayType.LINE;
  60. }
  61. export const getMetricDisplayType = (displayType: unknown): MetricDisplayType => {
  62. if (
  63. [MetricDisplayType.AREA, MetricDisplayType.BAR, MetricDisplayType.LINE].includes(
  64. displayType as MetricDisplayType
  65. )
  66. ) {
  67. return displayType as MetricDisplayType;
  68. }
  69. return MetricDisplayType.LINE;
  70. };
  71. export function getDdmUrl(
  72. orgSlug: string,
  73. {
  74. widgets,
  75. start,
  76. end,
  77. statsPeriod,
  78. project,
  79. ...otherParams
  80. }: Omit<DdmQueryParams, 'project' | 'widgets'> & {
  81. widgets: MetricWidgetQueryParams[];
  82. project?: (string | number)[];
  83. }
  84. ) {
  85. const urlParams: Partial<DdmQueryParams> = {
  86. ...otherParams,
  87. project: project?.map(id => (typeof id === 'string' ? parseInt(id, 10) : id)),
  88. widgets: JSON.stringify(widgets),
  89. };
  90. if (statsPeriod) {
  91. urlParams.statsPeriod = statsPeriod;
  92. } else {
  93. urlParams.start = start;
  94. urlParams.end = end;
  95. }
  96. return `/organizations/${orgSlug}/ddm/?${qs.stringify(urlParams)}`;
  97. }
  98. export function getMetricsApiRequestQuery(
  99. {field, query, groupBy, orderBy}: MetricsApiRequestMetric,
  100. {projects, environments, datetime}: PageFilters,
  101. {fidelity, ...overrides}: Partial<MetricsApiRequestQueryOptions> = {}
  102. ): MetricsApiRequestQuery {
  103. const {mri: mri} = parseField(field) ?? {};
  104. const useCase = getUseCaseFromMRI(mri) ?? 'custom';
  105. const interval = getDDMInterval(datetime, useCase, fidelity);
  106. const hasGroupBy = groupBy && groupBy.length > 0;
  107. const queryToSend = {
  108. ...getDateTimeParams(datetime),
  109. query,
  110. project: projects,
  111. environment: environments,
  112. field,
  113. useCase,
  114. interval,
  115. groupBy,
  116. orderBy: hasGroupBy && !orderBy && field ? `-${field}` : orderBy,
  117. useNewMetricsLayer: true,
  118. };
  119. return {...queryToSend, ...overrides};
  120. }
  121. const ddmHighFidelityLadder = new GranularityLadder([
  122. [SIXTY_DAYS, '1d'],
  123. [THIRTY_DAYS, '2h'],
  124. [TWO_WEEKS, '1h'],
  125. [ONE_WEEK, '30m'],
  126. [TWENTY_FOUR_HOURS, '5m'],
  127. [ONE_HOUR, '1m'],
  128. [0, '5m'],
  129. ]);
  130. const ddmLowFidelityLadder = new GranularityLadder([
  131. [SIXTY_DAYS, '1d'],
  132. [THIRTY_DAYS, '12h'],
  133. [TWO_WEEKS, '4h'],
  134. [ONE_WEEK, '2h'],
  135. [TWENTY_FOUR_HOURS, '1h'],
  136. [SIX_HOURS, '30m'],
  137. [ONE_HOUR, '5m'],
  138. [0, '1m'],
  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 getDDMInterval(
  142. datetimeObj: DateTimeObject,
  143. useCase: UseCase,
  144. fidelity: Fidelity = 'high'
  145. ) {
  146. const diffInMinutes = getDiffInMinutes(datetimeObj);
  147. if (diffInMinutes <= ONE_HOUR && useCase === 'custom' && fidelity === 'high') {
  148. return '10s';
  149. }
  150. if (fidelity === 'low') {
  151. return ddmLowFidelityLadder.getInterval(diffInMinutes);
  152. }
  153. return ddmHighFidelityLadder.getInterval(diffInMinutes);
  154. }
  155. export function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
  156. return period
  157. ? {statsPeriod: period}
  158. : {start: moment(start).toISOString(), end: moment(end).toISOString()};
  159. }
  160. export function getDefaultMetricOp(mri: MRI): MetricsOperation {
  161. const parsedMRI = parseMRI(mri);
  162. switch (parsedMRI?.type) {
  163. case 'd':
  164. case 'g':
  165. return 'avg';
  166. case 's':
  167. return 'count_unique';
  168. case 'c':
  169. default:
  170. return 'sum';
  171. }
  172. }
  173. export function isAllowedOp(op: string) {
  174. return !['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
  175. }
  176. // Applying these operations to a metric will result in a timeseries whose scale is different than
  177. // the original metric. Becuase of that min and max bounds can't be used and we display the fog of war
  178. export function isCumulativeOp(op: string = '') {
  179. return ['sum', 'count', 'count_unique'].includes(op);
  180. }
  181. export function updateQuery(
  182. router: InjectedRouter,
  183. queryUpdater:
  184. | Record<string, any>
  185. | ((query: Record<string, any>) => Record<string, any>)
  186. ) {
  187. router.push({
  188. ...router.location,
  189. query: {
  190. ...router.location.query,
  191. ...queryUpdater,
  192. },
  193. });
  194. }
  195. export function clearQuery(router: InjectedRouter) {
  196. router.push({
  197. ...router.location,
  198. query: {},
  199. });
  200. }
  201. export function useInstantRef<T>(value: T) {
  202. const ref = useRef(value);
  203. ref.current = value;
  204. return ref;
  205. }
  206. export function useUpdateQuery() {
  207. const router = useRouter();
  208. // Store the router in a ref so that we can use it in the callback
  209. // without needing to generate a new callback every time the location changes
  210. const routerRef = useInstantRef(router);
  211. return useCallback(
  212. (partialQuery: Record<string, any>) => {
  213. updateQuery(routerRef.current, partialQuery);
  214. },
  215. [routerRef]
  216. );
  217. }
  218. export function useClearQuery() {
  219. const router = useRouter();
  220. // Store the router in a ref so that we can use it in the callback
  221. // without needing to generate a new callback every time the location changes
  222. const routerRef = useInstantRef(router);
  223. return useCallback(() => {
  224. clearQuery(routerRef.current);
  225. }, [routerRef]);
  226. }
  227. // TODO(ddm): there has to be a nicer way to do this
  228. export function getSeriesName(
  229. group: MetricsGroup,
  230. isOnlyGroup = false,
  231. groupBy: MetricsQuery['groupBy']
  232. ) {
  233. if (isOnlyGroup && !groupBy?.length) {
  234. const field = Object.keys(group.series)?.[0];
  235. const {mri} = parseField(field) ?? {mri: field};
  236. const name = formatMRI(mri as MRI);
  237. return name ?? '(none)';
  238. }
  239. return Object.entries(group.by)
  240. .map(([key, value]) => `${key}:${String(value).length ? value : t('none')}`)
  241. .join(', ');
  242. }
  243. export function groupByOp(metrics: MetricMeta[]): Record<string, MetricMeta[]> {
  244. const uniqueOperations = [
  245. ...new Set(metrics.flatMap(field => field.operations).filter(isAllowedOp)),
  246. ].sort();
  247. const groupedByOp = uniqueOperations.reduce((result, op) => {
  248. result[op] = metrics.filter(field => field.operations.includes(op));
  249. return result;
  250. }, {});
  251. return groupedByOp;
  252. }
  253. export function isMeasurement({mri}: {mri: MRI}) {
  254. const {name} = parseMRI(mri) ?? {name: ''};
  255. return isMeasurementName(name);
  256. }
  257. export function isCustomMeasurement({mri}: {mri: MRI}) {
  258. const DEFINED_MEASUREMENTS = new Set(Object.keys(getMeasurements()));
  259. const {name} = parseMRI(mri) ?? {name: ''};
  260. return !DEFINED_MEASUREMENTS.has(name) && isMeasurementName(name);
  261. }
  262. export function isStandardMeasurement({mri}: {mri: MRI}) {
  263. return isMeasurement({mri}) && !isCustomMeasurement({mri});
  264. }
  265. export function isTransactionDuration({mri}: {mri: MRI}) {
  266. return mri === 'd:transactions/duration@millisecond';
  267. }
  268. export function isCustomMetric({mri}: {mri: MRI}) {
  269. return mri.includes(':custom/');
  270. }
  271. export function getFieldFromMetricsQuery(metricsQuery: MetricsQuery) {
  272. if (isCustomMetric(metricsQuery)) {
  273. return MRIToField(metricsQuery.mri, metricsQuery.op!);
  274. }
  275. return formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.op!));
  276. }
  277. // TODO(ddm): remove this and all of its usages once backend sends mri fields
  278. export function mapToMRIFields(
  279. data: MetricsApiResponse | undefined,
  280. fields: string[]
  281. ): void {
  282. if (!data) {
  283. return;
  284. }
  285. data.groups.forEach(group => {
  286. group.series = swapObjectKeys(group.series, fields);
  287. group.totals = swapObjectKeys(group.totals, fields);
  288. });
  289. }
  290. function swapObjectKeys(obj: Record<string, unknown> | undefined, newKeys: string[]) {
  291. if (!obj) {
  292. return {};
  293. }
  294. return Object.keys(obj).reduce((acc, key, index) => {
  295. acc[newKeys[index]] = obj[key];
  296. return acc;
  297. }, {});
  298. }
  299. export function stringifyMetricWidget(metricWidget: MetricsQuerySubject): string {
  300. const {mri, op, query, groupBy} = metricWidget;
  301. if (!op) {
  302. return '';
  303. }
  304. let result = `${op}(${formatMRI(mri)})`;
  305. if (query) {
  306. result += `{${query.trim()}}`;
  307. }
  308. if (groupBy && groupBy.length) {
  309. result += ` by ${groupBy.join(', ')}`;
  310. }
  311. return result;
  312. }
  313. // TODO: consider moving this to utils/dates.tsx
  314. export function getAbsoluteDateTimeRange(params: PageFilters['datetime']) {
  315. const {start, end, statsPeriod, utc} = normalizeDateTimeParams(params, {
  316. allowAbsoluteDatetime: true,
  317. });
  318. if (start && end) {
  319. return {start: moment(start).toISOString(), end: moment(end).toISOString()};
  320. }
  321. const parsedStatusPeriod = parseStatsPeriod(statsPeriod || '24h');
  322. const now = utc ? moment().utc() : moment();
  323. if (!parsedStatusPeriod) {
  324. // Default to 24h
  325. return {start: moment(now).subtract(1, 'day').toISOString(), end: now.toISOString()};
  326. }
  327. const startObj = moment(now).subtract(
  328. parsedStatusPeriod.period,
  329. parsedStatusPeriod.periodLength
  330. );
  331. return {start: startObj.toISOString(), end: now.toISOString()};
  332. }
  333. export function isSupportedDisplayType(displayType: unknown) {
  334. return Object.values(MetricDisplayType).includes(displayType as MetricDisplayType);
  335. }