123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import {useCallback, useRef} from 'react';
- import {InjectedRouter} from 'react-router';
- import moment from 'moment';
- import * as qs from 'query-string';
- import {
- DateTimeObject,
- Fidelity,
- 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 {MetricsApiResponse, PageFilters} from 'sentry/types';
- import type {
- MetricMeta,
- MetricsApiRequestMetric,
- MetricsApiRequestQuery,
- MetricsApiRequestQueryOptions,
- MetricsGroup,
- MetricsOperation,
- MRI,
- UseCase,
- } from 'sentry/types/metrics';
- import {isMeasurement as isMeasurementName} from 'sentry/utils/discover/fields';
- import {getMeasurements} from 'sentry/utils/measurements/measurements';
- import {
- formatMRI,
- formatMRIField,
- getUseCaseFromMRI,
- MRIToField,
- parseField,
- 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 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)}`;
- }
- export function getMetricsApiRequestQuery(
- {field, query, groupBy, orderBy}: MetricsApiRequestMetric,
- {projects, environments, datetime}: PageFilters,
- {fidelity, ...overrides}: Partial<MetricsApiRequestQueryOptions> = {}
- ): MetricsApiRequestQuery {
- const {mri: mri} = parseField(field) ?? {};
- const useCase = getUseCaseFromMRI(mri) ?? 'custom';
- const interval = getDDMInterval(datetime, useCase, fidelity);
- const hasGroupBy = groupBy && groupBy.length > 0;
- const queryToSend = {
- ...getDateTimeParams(datetime),
- query,
- project: projects,
- environment: environments,
- field,
- useCase,
- interval,
- groupBy,
- orderBy: hasGroupBy && !orderBy && field ? `-${field}` : orderBy,
- useNewMetricsLayer: true,
- };
- return {...queryToSend, ...overrides};
- }
- const ddmHighFidelityLadder = new GranularityLadder([
- [SIXTY_DAYS, '1d'],
- [THIRTY_DAYS, '2h'],
- [TWO_WEEKS, '1h'],
- [ONE_WEEK, '30m'],
- [TWENTY_FOUR_HOURS, '5m'],
- [ONE_HOUR, '1m'],
- [0, '5m'],
- ]);
- const ddmLowFidelityLadder = 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'],
- ]);
- // 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,
- fidelity: Fidelity = 'high'
- ) {
- const diffInMinutes = getDiffInMinutes(datetimeObj);
- if (diffInMinutes <= ONE_HOUR && useCase === 'custom' && fidelity === 'high') {
- return '10s';
- }
- if (fidelity === 'low') {
- return ddmLowFidelityLadder.getInterval(diffInMinutes);
- }
- return ddmHighFidelityLadder.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);
- }
- export function updateQuery(
- router: InjectedRouter,
- queryUpdater:
- | Record<string, any>
- | ((query: Record<string, any>) => Record<string, any>)
- ) {
- router.push({
- ...router.location,
- query: {
- ...router.location.query,
- ...queryUpdater,
- },
- });
- }
- 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>) => {
- updateQuery(routerRef.current, partialQuery);
- },
- [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]);
- }
- // TODO(ddm): there has to be a nicer way to do this
- export function getSeriesName(
- group: MetricsGroup,
- isOnlyGroup = false,
- groupBy: MetricsQuery['groupBy']
- ) {
- if (isOnlyGroup && !groupBy?.length) {
- const field = Object.keys(group.series)?.[0];
- const {mri} = parseField(field) ?? {mri: field};
- const name = formatMRI(mri as MRI);
- return name ?? '(none)';
- }
- return Object.entries(group.by)
- .map(([key, value]) => `${key}:${String(value).length ? value : t('none')}`)
- .join(', ');
- }
- 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 getFieldFromMetricsQuery(metricsQuery: MetricsQuery) {
- if (isCustomMetric(metricsQuery)) {
- return MRIToField(metricsQuery.mri, metricsQuery.op!);
- }
- return formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.op!));
- }
- // TODO(ddm): remove this and all of its usages once backend sends mri fields
- export function mapToMRIFields(
- data: MetricsApiResponse | undefined,
- fields: string[]
- ): void {
- if (!data) {
- return;
- }
- data.groups.forEach(group => {
- group.series = swapObjectKeys(group.series, fields);
- group.totals = swapObjectKeys(group.totals, fields);
- });
- }
- function swapObjectKeys(obj: Record<string, unknown> | undefined, newKeys: string[]) {
- if (!obj) {
- return {};
- }
- return Object.keys(obj).reduce((acc, key, index) => {
- acc[newKeys[index]] = obj[key];
- return acc;
- }, {});
- }
- 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 && 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 isSupportedDisplayType(displayType: unknown) {
- return Object.values(MetricDisplayType).includes(displayType as MetricDisplayType);
- }
|