123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- import {useCallback, useRef} from 'react';
- import type {InjectedRouter} from 'react-router';
- import moment from 'moment-timezone';
- import * as qs from 'query-string';
- import type {DateTimeObject} from 'sentry/components/charts/utils';
- import {
- getDiffInMinutes,
- GranularityLadder,
- ONE_HOUR,
- ONE_WEEK,
- SIX_HOURS,
- SIXTY_DAYS,
- THIRTY_DAYS,
- TWENTY_FOUR_HOURS,
- TWO_WEEKS,
- } from 'sentry/components/charts/utils';
- import {
- normalizeDateTimeParams,
- parseStatsPeriod,
- } from 'sentry/components/organizations/pageFilters/parse';
- import {t} from 'sentry/locale';
- import type {PageFilters} from 'sentry/types/core';
- import type {
- MetricAggregation,
- MetricMeta,
- MetricsDataIntervalLadder,
- MetricsQueryApiResponse,
- MetricsQueryApiResponseLastMeta,
- MRI,
- UseCase,
- } from 'sentry/types/metrics';
- import {isMeasurement} from 'sentry/utils/discover/fields';
- import {statsPeriodToDays} from 'sentry/utils/duration/statsPeriodToDays';
- import {getMeasurements} from 'sentry/utils/measurements/measurements';
- import {DEFAULT_AGGREGATES} from 'sentry/utils/metrics/constants';
- import {formatMRI, formatMRIField, MRIToField, parseMRI} from 'sentry/utils/metrics/mri';
- import type {
- MetricsQuery,
- MetricsQueryParams,
- MetricsWidget,
- } from 'sentry/utils/metrics/types';
- import {MetricDisplayType} from 'sentry/utils/metrics/types';
- import {
- isMetricFormula,
- type MetricsQueryApiQueryParams,
- } from 'sentry/utils/metrics/useMetricsQuery';
- import useRouter from 'sentry/utils/useRouter';
- export function getDefaultMetricDisplayType(
- mri?: MRI,
- aggregation?: MetricAggregation
- ): MetricDisplayType {
- if (mri?.startsWith('c') || aggregation === 'count') {
- return MetricDisplayType.BAR;
- }
- return MetricDisplayType.LINE;
- }
- export const getMetricDisplayType = (displayType: unknown): MetricDisplayType => {
- if (
- [MetricDisplayType.AREA, MetricDisplayType.BAR, MetricDisplayType.LINE].includes(
- displayType as MetricDisplayType
- )
- ) {
- return displayType as MetricDisplayType;
- }
- return MetricDisplayType.LINE;
- };
- export function getMetricsUrl(
- orgSlug: string,
- {
- widgets,
- start,
- end,
- statsPeriod,
- project,
- ...otherParams
- }: Omit<MetricsQueryParams, 'project' | 'widgets'> & {
- widgets: Partial<MetricsWidget>[];
- project?: (string | number)[];
- }
- ) {
- const urlParams: Partial<MetricsQueryParams> = {
- ...otherParams,
- project: project?.map(id => (typeof id === 'string' ? parseInt(id, 10) : id)),
- widgets: JSON.stringify(widgets),
- };
- if (statsPeriod) {
- urlParams.statsPeriod = statsPeriod;
- } else {
- urlParams.start = start;
- urlParams.end = end;
- }
- return `/organizations/${orgSlug}/metrics/?${qs.stringify(urlParams)}`;
- }
- const intervalLadders: Record<MetricsDataIntervalLadder, GranularityLadder> = {
- metrics: new GranularityLadder([
- [SIXTY_DAYS, '1d'],
- [THIRTY_DAYS, '2h'],
- [TWO_WEEKS, '1h'],
- [ONE_WEEK, '30m'],
- [TWENTY_FOUR_HOURS, '5m'],
- [ONE_HOUR, '1m'],
- [0, '1m'],
- ]),
- bar: new GranularityLadder([
- [SIXTY_DAYS, '1d'],
- [THIRTY_DAYS, '12h'],
- [TWO_WEEKS, '4h'],
- [ONE_WEEK, '2h'],
- [TWENTY_FOUR_HOURS, '1h'],
- [SIX_HOURS, '30m'],
- [ONE_HOUR, '5m'],
- [0, '1m'],
- ]),
- dashboard: new GranularityLadder([
- [SIXTY_DAYS, '1d'],
- [THIRTY_DAYS, '1h'],
- [TWO_WEEKS, '30m'],
- [ONE_WEEK, '30m'],
- [TWENTY_FOUR_HOURS, '5m'],
- [0, '5m'],
- ]),
- };
- // Wraps getInterval since other users of this function, and other metric use cases do not have support for 10s granularity
- export function getMetricsInterval(
- datetimeObj: DateTimeObject,
- useCase: UseCase,
- ladder: MetricsDataIntervalLadder = 'metrics'
- ) {
- const diffInMinutes = getDiffInMinutes(datetimeObj);
- if (diffInMinutes <= ONE_HOUR && useCase === 'custom' && ladder === 'metrics') {
- return '10s';
- }
- return intervalLadders[ladder].getInterval(diffInMinutes);
- }
- export function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
- return period
- ? {statsPeriod: period}
- : {start: moment(start).toISOString(), end: moment(end).toISOString()};
- }
- export function getDefaultAggregation(mri: MRI): MetricAggregation {
- const parsedMRI = parseMRI(mri);
- const fallbackAggregate = 'sum';
- if (!parsedMRI) {
- return fallbackAggregate;
- }
- return DEFAULT_AGGREGATES[parsedMRI.type] || fallbackAggregate;
- }
- // Using Records to ensure all MetricAggregations are covered
- const metricAggregationsCheck: Record<MetricAggregation, boolean> = {
- count: true,
- count_unique: true,
- sum: true,
- avg: true,
- min: true,
- max: true,
- p50: true,
- p75: true,
- p90: true,
- p95: true,
- p99: true,
- };
- export function isMetricsAggregation(value: string): value is MetricAggregation {
- return !!metricAggregationsCheck[value as MetricAggregation];
- }
- export function isAllowedAggregation(aggregation: MetricAggregation) {
- return !['max_timestamp', 'min_timestamp', 'histogram'].includes(aggregation);
- }
- // Applying these aggregations to a metric will result in a timeseries whose scale is different than
- // the original metric.
- export function isCumulativeAggregation(aggregation: MetricAggregation) {
- return ['sum', 'count', 'count_unique'].includes(aggregation);
- }
- function updateQuery(
- router: InjectedRouter,
- partialQuery: Record<string, any>,
- options?: {replace: boolean}
- ) {
- const updateFunction = options?.replace ? router.replace : router.push;
- updateFunction({
- ...router.location,
- query: {
- ...router.location.query,
- ...partialQuery,
- },
- });
- }
- export function clearQuery(router: InjectedRouter) {
- router.push({
- ...router.location,
- query: {},
- });
- }
- export function useInstantRef<T>(value: T) {
- const ref = useRef(value);
- ref.current = value;
- return ref;
- }
- export function useUpdateQuery() {
- const router = useRouter();
- // Store the router in a ref so that we can use it in the callback
- // without needing to generate a new callback every time the location changes
- const routerRef = useInstantRef(router);
- return useCallback(
- (partialQuery: Record<string, any>, options?: {replace: boolean}) => {
- updateQuery(routerRef.current, partialQuery, options);
- },
- [routerRef]
- );
- }
- export function useClearQuery() {
- const router = useRouter();
- // Store the router in a ref so that we can use it in the callback
- // without needing to generate a new callback every time the location changes
- const routerRef = useInstantRef(router);
- return useCallback(() => {
- clearQuery(routerRef.current);
- }, [routerRef]);
- }
- export function unescapeMetricsFormula(formula: string) {
- // Remove the $ from variable names
- return formula.replaceAll('$', '');
- }
- export function getMetricsSeriesName(
- query: MetricsQueryApiQueryParams,
- groupBy?: Record<string, string>,
- isMultiQuery: boolean = true
- ) {
- let name = getMetricQueryName(query);
- if (isMultiQuery) {
- name = `${query.name}: ${name}`;
- }
- const groupByEntries = Object.entries(groupBy ?? {});
- if (!groupByEntries || !groupByEntries.length) {
- return name;
- }
- const formattedGrouping = groupByEntries
- .map(([_key, value]) => `${String(value).length ? value : t('(none)')}`)
- .join(', ');
- if (isMultiQuery) {
- return `${name} - ${formattedGrouping}`;
- }
- return formattedGrouping;
- }
- export function getMetricQueryName(query: MetricsQueryApiQueryParams): string {
- return (
- query.alias ??
- (isMetricFormula(query)
- ? unescapeMetricsFormula(query.formula)
- : formatMRIField(MRIToField(query.mri, query.aggregation)))
- );
- }
- export function getMetricsSeriesId(
- query: MetricsQueryApiQueryParams,
- groupBy?: Record<string, string>
- ) {
- if (Object.keys(groupBy ?? {}).length === 0) {
- return `${query.name}`;
- }
- return `${query.name}-${JSON.stringify(groupBy)}`;
- }
- export function groupByOp(metrics: MetricMeta[]): Record<string, MetricMeta[]> {
- const uniqueOperations = [
- ...new Set(metrics.flatMap(field => field.operations).filter(isAllowedAggregation)),
- ].sort();
- const groupedByAggregation = uniqueOperations.reduce((result, aggregation) => {
- result[aggregation] = metrics.filter(field => field.operations.includes(aggregation));
- return result;
- }, {});
- return groupedByAggregation;
- }
- export function isTransactionMeasurement({mri}: {mri: MRI}) {
- const {name} = parseMRI(mri) ?? {name: ''};
- return isMeasurement(name);
- }
- export function isSpanMeasurement({mri}: {mri: MRI}) {
- if (
- mri === 'd:spans/http.response_content_length@byte' ||
- mri === 'd:spans/http.decoded_response_content_length@byte' ||
- mri === 'd:spans/http.response_transfer_size@byte'
- ) {
- return true;
- }
- const parsedMRI = parseMRI(mri);
- if (
- parsedMRI &&
- parsedMRI.useCase === 'spans' &&
- parsedMRI.name.startsWith('webvital.')
- ) {
- return true;
- }
- return false;
- }
- export function isCustomMeasurement({mri}: {mri: MRI}) {
- const DEFINED_MEASUREMENTS = new Set(Object.keys(getMeasurements()));
- const {name} = parseMRI(mri) ?? {name: ''};
- return !DEFINED_MEASUREMENTS.has(name) && isMeasurement(name);
- }
- export function isStandardMeasurement({mri}: {mri: MRI}) {
- return isTransactionMeasurement({mri}) && !isCustomMeasurement({mri});
- }
- export function isTransactionDuration({mri}: {mri: MRI}) {
- return mri === 'd:transactions/duration@millisecond';
- }
- export function isCustomMetric({mri}: {mri: MRI}) {
- return mri.includes(':custom/');
- }
- export function isVirtualMetric({mri}: {mri: MRI}) {
- return mri.startsWith('v:');
- }
- export function isCounterMetric({mri}: {mri: MRI}) {
- return mri.startsWith('c:');
- }
- export function isSpanDuration({mri}: {mri: MRI}) {
- return mri === 'd:spans/duration@millisecond';
- }
- export function getFieldFromMetricsQuery(metricsQuery: MetricsQuery) {
- if (isCustomMetric(metricsQuery)) {
- return MRIToField(metricsQuery.mri, metricsQuery.aggregation);
- }
- return formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.aggregation));
- }
- export function getFormattedMQL({
- mri,
- aggregation,
- query,
- groupBy,
- }: MetricsQuery): string {
- if (!aggregation) {
- return '';
- }
- let result = `${aggregation}(${formatMRI(mri)})`;
- if (query) {
- result += `{${query.trim()}}`;
- }
- if (groupBy?.length) {
- result += ` by ${groupBy.join(', ')}`;
- }
- return result;
- }
- export function isFormattedMQL(mql: string) {
- const regex = /^(\w+\([\w\.]+\))(?:\{\w+\:\w+\})*(?:\sby\s\w+)*/;
- const matches = mql.match(regex);
- const [, field, query, groupBy] = matches ?? [];
- if (!field) {
- return false;
- }
- if (query) {
- return query.includes(':');
- }
- if (groupBy) {
- // TODO check groupbys
- }
- return true;
- }
- // TODO: consider moving this to utils/dates.tsx
- export function getAbsoluteDateTimeRange(params: PageFilters['datetime']) {
- const {start, end, statsPeriod, utc} = normalizeDateTimeParams(params, {
- allowAbsoluteDatetime: true,
- });
- if (start && end) {
- return {start: moment(start).toISOString(), end: moment(end).toISOString()};
- }
- const parsedStatusPeriod = parseStatsPeriod(statsPeriod || '24h');
- const now = utc ? moment().utc() : moment();
- if (!parsedStatusPeriod) {
- // Default to 24h
- return {start: moment(now).subtract(1, 'day').toISOString(), end: now.toISOString()};
- }
- const startObj = moment(now).subtract(
- parsedStatusPeriod.period,
- parsedStatusPeriod.periodLength
- );
- return {start: startObj.toISOString(), end: now.toISOString()};
- }
- // TODO(metrics): remove this when we switch tags to the new meta
- export function getMetaDateTimeParams(datetime?: PageFilters['datetime']) {
- if (datetime?.period) {
- if (statsPeriodToDays(datetime.period) < 14) {
- return {statsPeriod: '14d'};
- }
- return {statsPeriod: datetime.period};
- }
- if (datetime?.start && datetime?.end) {
- return {
- start: moment(datetime.start).toISOString(),
- end: moment(datetime.end).toISOString(),
- };
- }
- return {statsPeriod: '14d'};
- }
- export function areResultsLimited(response: MetricsQueryApiResponse) {
- return response.meta.some(
- meta => (meta[meta.length - 1] as MetricsQueryApiResponseLastMeta).has_more
- );
- }
- export function isNotQueryOnly(query: MetricsQueryApiQueryParams) {
- return !('isQueryOnly' in query) || !query.isQueryOnly;
- }
|