mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
380 lines
10 KiB
TypeScript
380 lines
10 KiB
TypeScript
import {useCallback, useRef} from 'react';
|
|
import type {InjectedRouter} from 'react-router';
|
|
import moment from 'moment';
|
|
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 {Organization, PageFilters} from 'sentry/types';
|
|
import type {
|
|
MetricMeta,
|
|
MetricsDataIntervalLadder,
|
|
MetricsOperation,
|
|
MRI,
|
|
UseCase,
|
|
} from 'sentry/types/metrics';
|
|
import {statsPeriodToDays} from 'sentry/utils/dates';
|
|
import {isMeasurement as isMeasurementName} from 'sentry/utils/discover/fields';
|
|
import {generateEventSlug} from 'sentry/utils/discover/urls';
|
|
import {getMeasurements} from 'sentry/utils/measurements/measurements';
|
|
import {formatMRI, formatMRIField, MRIToField, parseMRI} from 'sentry/utils/metrics/mri';
|
|
import type {
|
|
DdmQueryParams,
|
|
MetricsQuery,
|
|
MetricsQuerySubject,
|
|
MetricWidgetQueryParams,
|
|
} from 'sentry/utils/metrics/types';
|
|
import {MetricDisplayType} from 'sentry/utils/metrics/types';
|
|
import {getTransactionDetailsUrl} from 'sentry/utils/performance/urls';
|
|
import useRouter from 'sentry/utils/useRouter';
|
|
|
|
export function getDefaultMetricDisplayType(
|
|
mri: MetricsQuery['mri'],
|
|
op: MetricsQuery['op']
|
|
): MetricDisplayType {
|
|
if (mri?.startsWith('c') || op === '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 getDdmUrl(
|
|
orgSlug: string,
|
|
{
|
|
widgets,
|
|
start,
|
|
end,
|
|
statsPeriod,
|
|
project,
|
|
...otherParams
|
|
}: Omit<DdmQueryParams, 'project' | 'widgets'> & {
|
|
widgets: MetricWidgetQueryParams[];
|
|
project?: (string | number)[];
|
|
}
|
|
) {
|
|
const urlParams: Partial<DdmQueryParams> = {
|
|
...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}/ddm/?${qs.stringify(urlParams)}`;
|
|
}
|
|
|
|
const intervalLadders: Record<MetricsDataIntervalLadder, GranularityLadder> = {
|
|
ddm: 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 getDDMInterval(
|
|
datetimeObj: DateTimeObject,
|
|
useCase: UseCase,
|
|
ladder: MetricsDataIntervalLadder = 'ddm'
|
|
) {
|
|
const diffInMinutes = getDiffInMinutes(datetimeObj);
|
|
|
|
if (diffInMinutes <= ONE_HOUR && useCase === 'custom' && ladder === 'ddm') {
|
|
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 getDefaultMetricOp(mri: MRI): MetricsOperation {
|
|
const parsedMRI = parseMRI(mri);
|
|
switch (parsedMRI?.type) {
|
|
case 'd':
|
|
case 'g':
|
|
return 'avg';
|
|
case 's':
|
|
return 'count_unique';
|
|
case 'c':
|
|
default:
|
|
return 'sum';
|
|
}
|
|
}
|
|
|
|
export function isAllowedOp(op: string) {
|
|
return !['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
|
|
}
|
|
|
|
// Applying these operations to a metric will result in a timeseries whose scale is different than
|
|
// the original metric. Becuase of that min and max bounds can't be used and we display the fog of war
|
|
export function isCumulativeOp(op: string = '') {
|
|
return ['sum', 'count', 'count_unique'].includes(op);
|
|
}
|
|
|
|
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 getMetricsSeriesName(
|
|
field: string,
|
|
groupBy?: Record<string, string>,
|
|
isMultiQuery: boolean = true
|
|
) {
|
|
const groupByEntries = Object.entries(groupBy ?? {});
|
|
|
|
if (!groupByEntries || !groupByEntries.length) {
|
|
return formatMRIField(field);
|
|
}
|
|
|
|
const formattedGrouping = groupByEntries
|
|
.map(([_key, value]) => `${String(value).length ? value : t('(none)')}`)
|
|
.join(', ');
|
|
|
|
if (isMultiQuery) {
|
|
return `${formatMRIField(field)} - ${formattedGrouping}`;
|
|
}
|
|
return formattedGrouping;
|
|
}
|
|
|
|
export function groupByOp(metrics: MetricMeta[]): Record<string, MetricMeta[]> {
|
|
const uniqueOperations = [
|
|
...new Set(metrics.flatMap(field => field.operations).filter(isAllowedOp)),
|
|
].sort();
|
|
|
|
const groupedByOp = uniqueOperations.reduce((result, op) => {
|
|
result[op] = metrics.filter(field => field.operations.includes(op));
|
|
return result;
|
|
}, {});
|
|
|
|
return groupedByOp;
|
|
}
|
|
|
|
export function isMeasurement({mri}: {mri: MRI}) {
|
|
const {name} = parseMRI(mri) ?? {name: ''};
|
|
return isMeasurementName(name);
|
|
}
|
|
|
|
export function isCustomMeasurement({mri}: {mri: MRI}) {
|
|
const DEFINED_MEASUREMENTS = new Set(Object.keys(getMeasurements()));
|
|
|
|
const {name} = parseMRI(mri) ?? {name: ''};
|
|
return !DEFINED_MEASUREMENTS.has(name) && isMeasurementName(name);
|
|
}
|
|
|
|
export function isStandardMeasurement({mri}: {mri: MRI}) {
|
|
return isMeasurement({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 isSpanMetric({mri}: {mri: MRI}) {
|
|
return mri.includes(':spans/');
|
|
}
|
|
|
|
export function getFieldFromMetricsQuery(metricsQuery: MetricsQuery) {
|
|
if (isCustomMetric(metricsQuery)) {
|
|
return MRIToField(metricsQuery.mri, metricsQuery.op!);
|
|
}
|
|
|
|
return formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.op!));
|
|
}
|
|
|
|
export function stringifyMetricWidget(metricWidget: MetricsQuerySubject): string {
|
|
const {mri, op, query, groupBy} = metricWidget;
|
|
|
|
if (!op) {
|
|
return '';
|
|
}
|
|
|
|
let result = `${op}(${formatMRI(mri)})`;
|
|
|
|
if (query) {
|
|
result += `{${query.trim()}}`;
|
|
}
|
|
|
|
if (groupBy?.length) {
|
|
result += ` by ${groupBy.join(', ')}`;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 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()};
|
|
}
|
|
|
|
export function getMetricsCorrelationSpanUrl(
|
|
organization: Organization,
|
|
projectSlug: string | undefined,
|
|
spanId: string | undefined,
|
|
transactionId: string,
|
|
transactionSpanId: string
|
|
) {
|
|
const isTransaction = spanId === transactionSpanId;
|
|
|
|
const eventSlug = generateEventSlug({
|
|
id: transactionId,
|
|
project: projectSlug,
|
|
});
|
|
|
|
return getTransactionDetailsUrl(
|
|
organization.slug,
|
|
eventSlug,
|
|
isTransaction ? transactionId : undefined,
|
|
{referrer: 'metrics', openPanel: 'open'},
|
|
isTransaction ? undefined : spanId
|
|
);
|
|
}
|
|
|
|
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'};
|
|
}
|