Repository: getsentry/sentry
Pull request: feat(dashboards): ddm chart intervals
- closes #61029 

Note: Consolidating chart intervals cross-dataset is not easily achievable across different datasets, this PR aims to align metric widget intervals with other dashboard widgets.

<img width="1212" alt="image" src="https://github.com/getsentry/sentry/assets/86684834/8e7a9f71-202e-41cb-8ed4-19b2ad3170ea">


Problem statement:
Consolidate chart intervals
As part of this ticket, we did an audit of the most common timeframes and optimized for a consistent number of buckets, consistent charts across datasets/products, and developer readability. Here are the desired results:

| Timeframe  | Interval |
| ------------- | ------------- |
| 1h  | 10s/1m  |
| 24h  | 5m  |
| 7d  | 30m  |
| 14d  | 1h  |
| 30d  | 2h  |
| 90d  | 1d  |

For bar charts, it needs to be significantly less granular.

These are the changes that need to be made:
- Dashboards on 1h change from 5m to 1m
- Starfish on 24h change from 30m to 5m
- Starfish, DDM, Performance on 14d change from 4h to 1h
- Discover, Dashboards on 14d change from 30m to 1h
- Starfish, DDM, Performance on 30d change from 12h to 2h
- Discover, Dashboards on 30d change from 1h to 2h
- Discover, Dashboards on 90d change from 4h to 1d

Hints from issue discussion:

Commit message: fix bar charts

Patch to review:
diff --git a/static/app/types/metrics.tsx b/static/app/types/metrics.tsx
index 0d710b098f6e75..e4d77d07836613 100644
--- a/static/app/types/metrics.tsx
+++ b/static/app/types/metrics.tsx
@@ -43,9 +43,11 @@ export type MetricsApiRequestQuery = MetricsApiRequestMetric & {
   statsPeriod?: string;
 };
 
-export type MetricsApiRequestQueryOptions = Partial<MetricsApiRequestQuery> & {
-  fidelity?: 'high' | 'low';
-};
+export type MetricsDataIntervalLadder = 'ddm' | 'bar' | 'dashboard';
+
+export interface MetricsApiRequestQueryOptions extends Partial<MetricsApiRequestQuery> {
+  intervalLadder?: MetricsDataIntervalLadder;
+}
 
 export type MetricsApiResponse = {
   end: string;
diff --git a/static/app/utils/metrics/index.tsx b/static/app/utils/metrics/index.tsx
index 56f60a969fc9b1..4d8e3093e50c04 100644
--- a/static/app/utils/metrics/index.tsx
+++ b/static/app/utils/metrics/index.tsx
@@ -3,7 +3,7 @@ import type {InjectedRouter} from 'react-router';
 import moment from 'moment';
 import * as qs from 'query-string';
 
-import type {DateTimeObject, Fidelity} from 'sentry/components/charts/utils';
+import type {DateTimeObject} from 'sentry/components/charts/utils';
 import {
   getDiffInMinutes,
   GranularityLadder,
@@ -26,6 +26,7 @@ import type {
   MetricsApiRequestMetric,
   MetricsApiRequestQuery,
   MetricsApiRequestQueryOptions,
+  MetricsDataIntervalLadder,
   MetricsGroup,
   MetricsOperation,
   MRI,
@@ -108,11 +109,11 @@ export function getDdmUrl(
 export function getMetricsApiRequestQuery(
   {field, query, groupBy, orderBy}: MetricsApiRequestMetric,
   {projects, environments, datetime}: PageFilters,
-  {fidelity, ...overrides}: Partial<MetricsApiRequestQueryOptions> = {}
+  {intervalLadder, ...overrides}: Partial<MetricsApiRequestQueryOptions> = {}
 ): MetricsApiRequestQuery {
   const {mri: mri} = parseField(field) ?? {};
   const useCase = getUseCaseFromMRI(mri) ?? 'custom';
-  const interval = getDDMInterval(datetime, useCase, fidelity);
+  const interval = getDDMInterval(datetime, useCase, intervalLadder);
 
   const hasGroupBy = groupBy && groupBy.length > 0;
 
@@ -136,44 +137,49 @@ function sanitizeQuery(query?: string) {
   return query?.trim();
 }
 
-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'],
-]);
+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, '5m'],
+  ]),
+  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,
-  fidelity: Fidelity = 'high'
+  ladder: MetricsDataIntervalLadder = 'ddm'
 ) {
   const diffInMinutes = getDiffInMinutes(datetimeObj);
 
-  if (diffInMinutes <= ONE_HOUR && useCase === 'custom' && fidelity === 'high') {
+  if (diffInMinutes <= ONE_HOUR && useCase === 'custom' && ladder === 'ddm') {
     return '10s';
   }
 
-  if (fidelity === 'low') {
-    return ddmLowFidelityLadder.getInterval(diffInMinutes);
-  }
-
-  return ddmHighFidelityLadder.getInterval(diffInMinutes);
+  return intervalLadders[ladder].getInterval(diffInMinutes);
 }
 
 export function getDateTimeParams({start, end, period}: PageFilters['datetime']) {
diff --git a/static/app/views/dashboards/widgetCard/metricWidgetCard/index.tsx b/static/app/views/dashboards/widgetCard/metricWidgetCard/index.tsx
index 969f7a64a8843f..a1eb1f77c8a502 100644
--- a/static/app/views/dashboards/widgetCard/metricWidgetCard/index.tsx
+++ b/static/app/views/dashboards/widgetCard/metricWidgetCard/index.tsx
@@ -14,10 +14,7 @@ import {space} from 'sentry/styles/space';
 import type {MRI, Organization, PageFilters} from 'sentry/types';
 import type {ReactEchartsRef} from 'sentry/types/echarts';
 import {stringifyMetricWidget} from 'sentry/utils/metrics';
-import {
-  MetricDisplayType,
-  type MetricWidgetQueryParams,
-} from 'sentry/utils/metrics/types';
+import type {MetricWidgetQueryParams} from 'sentry/utils/metrics/types';
 import {useMetricsDataZoom} from 'sentry/utils/metrics/useMetricsData';
 import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard';
 import type {AugmentedEChartDataZoomHandler} from 'sentry/views/dashboards/widgetCard/chart';
@@ -232,7 +229,7 @@ export function MetricWidgetChartContainer({
       environments,
       datetime,
     },
-    {fidelity: displayType === MetricDisplayType.BAR ? 'low' : 'high'}
+    {intervalLadder: 'dashboard'}
   );
 
   const chartRef = useRef<ReactEchartsRef>(null);
diff --git a/static/app/views/ddm/widget.tsx b/static/app/views/ddm/widget.tsx
index 69c26252a9bc80..3f55f0b3ebedaa 100644
--- a/static/app/views/ddm/widget.tsx
+++ b/static/app/views/ddm/widget.tsx
@@ -255,7 +255,7 @@ const MetricWidgetBody = memo(
         environments,
         datetime,
       },
-      {fidelity: displayType === MetricDisplayType.BAR ? 'low' : 'high'}
+      {intervalLadder: displayType === MetricDisplayType.BAR ? 'bar' : 'ddm'}
     );
 
     const chartRef = useRef<ReactEchartsRef>(null);


Code review comments:
diff_hunk: @@ -136,44 +137,49 @@ function sanitizeQuery(query?: string) {
   return query?.trim();
 }
 
-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'],
-]);
+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, '5m'],
path: static/app/utils/metrics/index.tsx
text: @user1:
Why is `ONE_HOUR -> 1m` but `0 -> 5m`?

@author:
Copy-pasta :D