Browse Source

feat(crons): Allow timeline view to be used with arbitrary start/end (#54872)

Allows the timeline view to operate off an arbitrary start/end or period
from the page date filter. The main change now is that many of the
timeline components previously worked off a `timeWindow`: `1h`, `24h`,
etc... which they would use as a key to retrieve a `timeWindowConfig`
which held information about how far apart time labels should be, what
date info to show in tooltips, etc...

Now we use start/end to generate a config which is also dynamic based on
the width of the timeline view. We have a number of preset time
intervals to space labels with such as individual minutes, 10 minutes,
30 minutes, etc... going up to `N` number of days between each label.

This will eventually pave the way for the timeline view to be added onto
the monitor details page.
David Wang 1 year ago
parent
commit
fa9e53dd13

+ 9 - 7
static/app/components/events/interfaces/crons/cronTimelineSection.tsx

@@ -22,7 +22,7 @@ import {
   MonitorBucketData,
   TimeWindow,
 } from 'sentry/views/monitors/components/overviewTimeline/types';
-import {timeWindowConfig} from 'sentry/views/monitors/components/overviewTimeline/utils';
+import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils';
 import {getTimeRangeFromEvent} from 'sentry/views/monitors/utils/getTimeRangeFromEvent';
 
 interface Props {
@@ -43,8 +43,8 @@ export function CronTimelineSection({event, organization}: Props) {
   const elementRef = useRef<HTMLDivElement>(null);
   const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
 
-  const elapsedMinutes = timeWindowConfig[timeWindow].elapsedMinutes;
-  const rollup = Math.floor((elapsedMinutes * 60) / timelineWidth);
+  const timeWindowConfig = getConfigFromTimeRange(start, end, timelineWidth);
+  const rollup = Math.floor((timeWindowConfig.elapsedMinutes * 60) / timelineWidth);
 
   const monitorStatsQueryKey = `/organizations/${organization.slug}/monitors-stats/`;
   const {data: monitorStats, isLoading} = useApiQuery<Record<string, MonitorBucketData>>(
@@ -69,7 +69,7 @@ export function CronTimelineSection({event, organization}: Props) {
     return null;
   }
 
-  const msPerPixel = (elapsedMinutes * 60 * 1000) / timelineWidth;
+  const msPerPixel = (timeWindowConfig.elapsedMinutes * 60 * 1000) / timelineWidth;
   const eventTickLeft =
     (new Date(event.dateReceived).valueOf() - start.valueOf()) / msPerPixel;
 
@@ -83,13 +83,15 @@ export function CronTimelineSection({event, organization}: Props) {
       <TimelineContainer>
         <TimelineWidthTracker ref={elementRef} />
         <StyledGridLineTimeLabels
-          timeWindow={timeWindow}
+          timeWindowConfig={timeWindowConfig}
+          start={start}
           end={end}
           width={timelineWidth}
         />
         <StyledGridLineOverlay
           showCursor={!isLoading}
-          timeWindow={timeWindow}
+          timeWindowConfig={timeWindowConfig}
+          start={start}
           end={end}
           width={timelineWidth}
         />
@@ -105,7 +107,7 @@ export function CronTimelineSection({event, organization}: Props) {
                 bucketedData={monitorStats[monitorSlug]}
                 start={start}
                 end={end}
-                timeWindow={timeWindow}
+                timeWindowConfig={timeWindowConfig}
                 environment={environment ?? DEFAULT_ENVIRONMENT}
               />
             </FadeInContainer>

+ 4 - 4
static/app/views/monitors/components/overviewTimeline/checkInTimeline.tsx

@@ -6,14 +6,14 @@ import {getAggregateStatus} from 'sentry/views/monitors/utils/getAggregateStatus
 import {mergeBuckets} from 'sentry/views/monitors/utils/mergeBuckets';
 
 import {JobTickTooltip} from './jobTickTooltip';
-import {MonitorBucketData, TimeWindow} from './types';
+import {MonitorBucketData, TimeWindowOptions} from './types';
 
 export interface CheckInTimelineProps {
   bucketedData: MonitorBucketData;
   end: Date;
   environment: string;
   start: Date;
-  timeWindow: TimeWindow;
+  timeWindowConfig: TimeWindowOptions;
   width: number;
 }
 
@@ -27,7 +27,7 @@ function getBucketedCheckInsPosition(
 }
 
 export function CheckInTimeline(props: CheckInTimelineProps) {
-  const {bucketedData, start, end, timeWindow, width, environment} = props;
+  const {bucketedData, start, end, timeWindowConfig, width, environment} = props;
 
   const elapsedMs = end.getTime() - start.getTime();
   const msPerPixel = elapsedMs / width;
@@ -50,7 +50,7 @@ export function CheckInTimeline(props: CheckInTimelineProps) {
         return (
           <JobTickTooltip
             jobTick={jobTick}
-            timeWindow={timeWindow}
+            timeWindowConfig={timeWindowConfig}
             skipWrapper
             key={startTs}
           >

+ 47 - 33
static/app/views/monitors/components/overviewTimeline/gridLines.tsx

@@ -4,31 +4,31 @@ import moment from 'moment';
 
 import DateTime from 'sentry/components/dateTime';
 import {space} from 'sentry/styles/space';
-import {TimeWindow} from 'sentry/views/monitors/components/overviewTimeline/types';
-import {
-  getStartFromTimeWindow,
-  timeWindowConfig,
-} from 'sentry/views/monitors/components/overviewTimeline/utils';
+import {TimeWindowOptions} from 'sentry/views/monitors/components/overviewTimeline/types';
 
 import {useTimelineCursor} from './timelineCursor';
 
 interface Props {
   end: Date;
-  timeWindow: TimeWindow;
+  start: Date;
+  timeWindowConfig: TimeWindowOptions;
   width: number;
   className?: string;
   showCursor?: boolean;
   stickyCursor?: boolean;
 }
 
-function clampTimeBasedOnResolution(date: moment.Moment, resolution: string) {
-  date.startOf('minute');
-  if (resolution === '1h') {
-    date.minute(date.minutes() - (date.minutes() % 10));
-  } else if (resolution === '30d') {
-    date.startOf('day');
-  } else {
+/**
+ * Aligns a date to a clean offset such as start of minute, hour, day
+ * based on the interval of how far each time label is placed.
+ */
+function alignTimeMarkersToStartOf(date: moment.Moment, timeMarkerInterval: number) {
+  if (timeMarkerInterval < 60) {
+    date.minute(date.minutes() - (date.minutes() % timeMarkerInterval));
+  } else if (timeMarkerInterval < 60 * 24) {
     date.startOf('hour');
+  } else {
+    date.startOf('day');
   }
 }
 
@@ -37,18 +37,22 @@ interface TimeMarker {
   position: number;
 }
 
-function getTimeMarkers(end: Date, timeWindow: TimeWindow, width: number): TimeMarker[] {
-  const {elapsedMinutes, timeMarkerInterval} = timeWindowConfig[timeWindow];
+function getTimeMarkersFromConfig(
+  start: Date,
+  end: Date,
+  config: TimeWindowOptions,
+  width: number
+) {
+  const {elapsedMinutes, timeMarkerInterval} = config;
   const msPerPixel = (elapsedMinutes * 60 * 1000) / width;
 
   const times: TimeMarker[] = [];
-  const start = getStartFromTimeWindow(end, timeWindow);
 
-  const firstTimeMark = moment(start);
-  clampTimeBasedOnResolution(firstTimeMark, timeWindow);
+  const lastTimeMark = moment(end);
+  alignTimeMarkersToStartOf(lastTimeMark, timeMarkerInterval);
   // Generate time markers which represent location of grid lines/time labels
   for (let i = 1; i < elapsedMinutes / timeMarkerInterval; i++) {
-    const timeMark = moment(firstTimeMark).add(i * timeMarkerInterval, 'minute');
+    const timeMark = moment(lastTimeMark).subtract(i * timeMarkerInterval, 'minute');
     const position = (timeMark.valueOf() - start.valueOf()) / msPerPixel;
     times.push({date: timeMark.toDate(), position});
   }
@@ -56,36 +60,44 @@ function getTimeMarkers(end: Date, timeWindow: TimeWindow, width: number): TimeM
   return times;
 }
 
-export function GridLineTimeLabels({end, timeWindow, width, className}: Props) {
+export function GridLineTimeLabels({
+  width,
+  timeWindowConfig,
+  start,
+  end,
+  className,
+}: Props) {
   return (
     <LabelsContainer className={className}>
-      {getTimeMarkers(end, timeWindow, width).map(({date, position}) => (
-        <TimeLabelContainer key={date.getTime()} left={position}>
-          <TimeLabel date={date} {...timeWindowConfig[timeWindow].dateTimeProps} />
-        </TimeLabelContainer>
-      ))}
+      {getTimeMarkersFromConfig(start, end, timeWindowConfig, width).map(
+        ({date, position}) => (
+          <TimeLabelContainer key={date.getTime()} left={position}>
+            <TimeLabel date={date} {...timeWindowConfig.dateTimeProps} />
+          </TimeLabelContainer>
+        )
+      )}
     </LabelsContainer>
   );
 }
 
 export function GridLineOverlay({
   end,
-  timeWindow,
   width,
+  timeWindowConfig,
+  start,
   showCursor,
   stickyCursor,
   className,
 }: Props) {
-  const {cursorLabelFormat} = timeWindowConfig[timeWindow];
+  const {dateLabelFormat} = timeWindowConfig;
 
   const makeCursorText = useCallback(
     (percentPosition: number) => {
-      const start = getStartFromTimeWindow(end, timeWindow);
       const timeOffset = (end.getTime() - start.getTime()) * percentPosition;
 
-      return moment(start.getTime() + timeOffset).format(cursorLabelFormat);
+      return moment(start.getTime() + timeOffset).format(dateLabelFormat);
     },
-    [cursorLabelFormat, end, timeWindow]
+    [dateLabelFormat, end, start]
   );
 
   const {cursorContainerRef, timelineCursor} = useTimelineCursor<HTMLDivElement>({
@@ -98,9 +110,11 @@ export function GridLineOverlay({
     <Overlay ref={cursorContainerRef} className={className}>
       {timelineCursor}
       <GridLineContainer>
-        {getTimeMarkers(end, timeWindow, width).map(({date, position}) => (
-          <Gridline key={date.getTime()} left={position} />
-        ))}
+        {getTimeMarkersFromConfig(start, end, timeWindowConfig, width).map(
+          ({date, position}) => (
+            <Gridline key={date.getTime()} left={position} />
+          )
+        )}
       </GridLineContainer>
     </Overlay>
   );

+ 9 - 8
static/app/views/monitors/components/overviewTimeline/index.tsx

@@ -18,7 +18,7 @@ import {Monitor} from '../../types';
 import {ResolutionSelector} from './resolutionSelector';
 import {TimelineTableRow} from './timelineTableRow';
 import {MonitorBucketData, TimeWindow} from './types';
-import {getStartFromTimeWindow, timeWindowConfig} from './utils';
+import {getConfigFromTimeRange, getStartFromTimeWindow} from './utils';
 
 interface Props {
   monitorList: Monitor[];
@@ -34,9 +34,8 @@ export function OverviewTimeline({monitorList}: Props) {
   const elementRef = useRef<HTMLDivElement>(null);
   const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
 
-  const rollup = Math.floor(
-    (timeWindowConfig[timeWindow].elapsedMinutes * 60) / timelineWidth
-  );
+  const timeWindowConfig = getConfigFromTimeRange(start, nowRef.current, timelineWidth);
+  const rollup = Math.floor((timeWindowConfig.elapsedMinutes * 60) / timelineWidth);
   const monitorStatsQueryKey = `/organizations/${organization.slug}/monitors-stats/`;
   const {data: monitorStats, isLoading} = useApiQuery<Record<string, MonitorBucketData>>(
     [
@@ -65,7 +64,8 @@ export function OverviewTimeline({monitorList}: Props) {
       </StickyResolutionSelector>
       <StickyGridLineTimeLabels>
         <BorderlessGridLineTimeLabels
-          timeWindow={timeWindow}
+          timeWindowConfig={timeWindowConfig}
+          start={start}
           end={nowRef.current}
           width={timelineWidth}
         />
@@ -73,7 +73,8 @@ export function OverviewTimeline({monitorList}: Props) {
       <GridLineOverlay
         stickyCursor
         showCursor={!isLoading}
-        timeWindow={timeWindow}
+        timeWindowConfig={timeWindowConfig}
+        start={start}
         end={nowRef.current}
         width={timelineWidth}
       />
@@ -82,10 +83,10 @@ export function OverviewTimeline({monitorList}: Props) {
         <TimelineTableRow
           key={monitor.id}
           monitor={monitor}
-          timeWindow={timeWindow}
+          timeWindowConfig={timeWindowConfig}
+          start={start}
           bucketedData={monitorStats?.[monitor.slug]}
           end={nowRef.current}
-          start={start}
           width={timelineWidth}
         />
       ))}

+ 18 - 3
static/app/views/monitors/components/overviewTimeline/jobTickTooltip.spec.tsx

@@ -1,6 +1,8 @@
 import {render, screen, within} from 'sentry-test/reactTestingLibrary';
 
+import {getFormat} from 'sentry/utils/dates';
 import {JobTickTooltip} from 'sentry/views/monitors/components/overviewTimeline/jobTickTooltip';
+import {TimeWindowOptions} from 'sentry/views/monitors/components/overviewTimeline/types';
 
 type StatusCounts = [ok: number, missed: number, timeout: number, error: number];
 
@@ -11,6 +13,13 @@ export function generateEnvMapping(name: string, counts: StatusCounts) {
   };
 }
 
+const tickConfig: TimeWindowOptions = {
+  dateLabelFormat: getFormat({timeOnly: true, seconds: true}),
+  elapsedMinutes: 60,
+  timeMarkerInterval: 10,
+  dateTimeProps: {timeOnly: true},
+};
+
 describe('JobTickTooltip', function () {
   it('renders tooltip representing single job run', function () {
     const startTs = new Date('2023-06-15T11:00:00Z').valueOf();
@@ -25,7 +34,9 @@ describe('JobTickTooltip', function () {
       width: 4,
     };
 
-    render(<JobTickTooltip jobTick={jobTick} timeWindow="1h" forceVisible />);
+    render(
+      <JobTickTooltip jobTick={jobTick} timeWindowConfig={tickConfig} forceVisible />
+    );
 
     // Skip the header row
     const statusRow = screen.getAllByRole('row')[1];
@@ -48,7 +59,9 @@ describe('JobTickTooltip', function () {
       width: 4,
     };
 
-    render(<JobTickTooltip jobTick={jobTick} timeWindow="1h" forceVisible />);
+    render(
+      <JobTickTooltip jobTick={jobTick} timeWindowConfig={tickConfig} forceVisible />
+    );
 
     const okayRow = screen.getAllByRole('row')[1];
     expect(within(okayRow).getByText('Okay')).toBeInTheDocument();
@@ -86,7 +99,9 @@ describe('JobTickTooltip', function () {
       width: 4,
     };
 
-    render(<JobTickTooltip jobTick={jobTick} timeWindow="1h" forceVisible />);
+    render(
+      <JobTickTooltip jobTick={jobTick} timeWindowConfig={tickConfig} forceVisible />
+    );
 
     const missedProdRow = screen.getAllByRole('row')[1];
     expect(within(missedProdRow).getByText('Missed')).toBeInTheDocument();

+ 6 - 8
static/app/views/monitors/components/overviewTimeline/jobTickTooltip.tsx

@@ -8,21 +8,19 @@ import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {
   JobTickData,
-  TimeWindow,
+  TimeWindowOptions,
 } from 'sentry/views/monitors/components/overviewTimeline/types';
 import {CheckInStatus} from 'sentry/views/monitors/types';
 import {getColorsFromStatus, statusToText} from 'sentry/views/monitors/utils';
 
-import {timeWindowConfig} from './utils';
-
 interface Props extends Omit<TooltipProps, 'title'> {
   jobTick: JobTickData;
-  timeWindow: TimeWindow;
+  timeWindowConfig: TimeWindowOptions;
 }
 
-export function JobTickTooltip({jobTick, timeWindow, children, ...props}: Props) {
+export function JobTickTooltip({jobTick, timeWindowConfig, children, ...props}: Props) {
   const {startTs, endTs, envMapping} = jobTick;
-  const {dateTimeProps} = timeWindowConfig[timeWindow];
+  const {dateLabelFormat} = timeWindowConfig;
   const capturedEnvs = Object.keys(envMapping);
   const representsSingleJob =
     capturedEnvs.length === 1 &&
@@ -32,11 +30,11 @@ export function JobTickTooltip({jobTick, timeWindow, children, ...props}: Props)
   const tooltipTitle = (
     <Fragment>
       <TooltipTimeLabel>
-        <DateTime date={startTs * 1000} {...dateTimeProps} />
+        <DateTime date={startTs * 1000} format={dateLabelFormat} />
         {!representsSingleJob && (
           <Fragment>
             <Text>{'\u2014'}</Text>
-            <DateTime date={endTs * 1000} {...dateTimeProps} />
+            <DateTime date={endTs * 1000} format={dateLabelFormat} />
           </Fragment>
         )}
       </TooltipTimeLabel>

+ 2 - 4
static/app/views/monitors/components/overviewTimeline/types.tsx

@@ -4,9 +4,9 @@ export type TimeWindow = '1h' | '24h' | '7d' | '30d';
 
 export interface TimeWindowOptions {
   /**
-   * The time format used for the cursor label
+   * The time format used for the cursor label and job tick tooltip
    */
-  cursorLabelFormat: string;
+  dateLabelFormat: string;
   /**
    * Props to pass to <DateTime> when displaying a time marker
    */
@@ -21,8 +21,6 @@ export interface TimeWindowOptions {
   timeMarkerInterval: number;
 }
 
-export type TimeWindowData = Record<TimeWindow, TimeWindowOptions>;
-
 // TODO(davidenwang): Remove this type as its a little too specific
 export type MonitorBucketData = MonitorBucket[];
 

+ 48 - 3
static/app/views/monitors/components/overviewTimeline/utils.spec.tsx

@@ -1,9 +1,12 @@
-import {getStartFromTimeWindow} from 'sentry/views/monitors/components/overviewTimeline/utils';
+import {getFormat} from 'sentry/utils/dates';
+import {
+  getConfigFromTimeRange,
+  getStartFromTimeWindow,
+} from 'sentry/views/monitors/components/overviewTimeline/utils';
 
 describe('Crons Timeline Utils', function () {
-  const end = new Date('2023-06-15T12:00:00Z');
-
   describe('getStartFromTimeWindow', function () {
+    const end = new Date('2023-06-15T12:00:00Z');
     it('correctly computes for 1h', function () {
       const expectedStart = new Date('2023-06-15T11:00:00Z');
       const start = getStartFromTimeWindow(end, '1h');
@@ -32,4 +35,46 @@ describe('Crons Timeline Utils', function () {
       expect(start).toEqual(expectedStart);
     });
   });
+
+  describe('getConfigFromTimeRange', function () {
+    const timelineWidth = 800;
+
+    it('divides into minutes for small intervals', function () {
+      const start = new Date('2023-06-15T11:00:00Z');
+      const end = new Date('2023-06-15T11:05:00Z');
+      const config = getConfigFromTimeRange(start, end, timelineWidth);
+      expect(config).toEqual({
+        dateLabelFormat: getFormat({timeOnly: true, seconds: true}),
+        elapsedMinutes: 5,
+        timeMarkerInterval: 1,
+        dateTimeProps: {timeOnly: true},
+      });
+    });
+
+    it('divides into minutes without showing seconds for medium intervals', function () {
+      const start = new Date('2023-06-15T08:00:00Z');
+      const end = new Date('2023-06-15T23:00:00Z');
+      const config = getConfigFromTimeRange(start, end, timelineWidth);
+      expect(config).toEqual({
+        dateLabelFormat: getFormat({timeOnly: true}),
+        elapsedMinutes: 900,
+        timeMarkerInterval: 240,
+        dateTimeProps: {timeOnly: true},
+      });
+    });
+
+    it('divides into days for larger intervals', function () {
+      const start = new Date('2023-05-15T11:00:00Z');
+      const end = new Date('2023-06-15T11:00:00Z');
+      const config = getConfigFromTimeRange(start, end, timelineWidth);
+      expect(config).toEqual({
+        dateLabelFormat: getFormat(),
+        // 31 elapsed days
+        elapsedMinutes: 31 * 24 * 60,
+        // 4 days in between each time label
+        timeMarkerInterval: 4 * 24 * 60,
+        dateTimeProps: {dateOnly: true},
+      });
+    });
+  });
 });

+ 44 - 30
static/app/views/monitors/components/overviewTimeline/utils.tsx

@@ -2,39 +2,53 @@ import moment from 'moment';
 
 import {getFormat} from 'sentry/utils/dates';
 
-import {TimeWindow, TimeWindowData} from './types';
-
-// Stores options and data which correspond to each selectable time window
-export const timeWindowConfig: TimeWindowData = {
-  '1h': {
-    cursorLabelFormat: getFormat({timeOnly: true, seconds: true}),
-    elapsedMinutes: 60,
-    timeMarkerInterval: 10,
-    dateTimeProps: {timeOnly: true},
-  },
-  '24h': {
-    cursorLabelFormat: getFormat({timeOnly: true}),
-    elapsedMinutes: 60 * 24,
-    timeMarkerInterval: 60 * 4,
-    dateTimeProps: {timeOnly: true},
-  },
-  '7d': {
-    cursorLabelFormat: getFormat(),
-    elapsedMinutes: 60 * 24 * 7,
-    timeMarkerInterval: 60 * 24,
-    dateTimeProps: {},
-  },
-  '30d': {
-    cursorLabelFormat: getFormat({dateOnly: true}),
-    elapsedMinutes: 60 * 24 * 30,
-    timeMarkerInterval: 60 * 24 * 5,
-    dateTimeProps: {dateOnly: true},
-  },
+import {TimeWindow, TimeWindowOptions} from './types';
+
+// Stores the elapsed minutes for each selectable resolution
+export const resolutionElapsedMinutes: Record<TimeWindow, number> = {
+  '1h': 60,
+  '24h': 60 * 24,
+  '7d': 60 * 24 * 7,
+  '30d': 60 * 24 * 30,
 };
 
 export function getStartFromTimeWindow(end: Date, timeWindow: TimeWindow): Date {
-  const {elapsedMinutes} = timeWindowConfig[timeWindow];
-  const start = moment(end).subtract(elapsedMinutes, 'minute');
+  const start = moment(end).subtract(resolutionElapsedMinutes[timeWindow], 'minute');
 
   return start.toDate();
 }
+
+// The pixels to allocate to each time label based on (MMM DD HH:SS AM/PM)
+const TIMELABEL_WIDTH = 100;
+
+export function getConfigFromTimeRange(
+  start: Date,
+  end: Date,
+  timelineWidth: number
+): TimeWindowOptions {
+  // Acceptable intervals between time labels, in minutes
+  const minuteRanges = [1, 10, 30, 60, 4 * 60, 8 * 60, 12 * 60];
+  const startEndMinutes = (end.getTime() - start.getTime()) / (1000 * 60);
+  const timeLabelMinutes = startEndMinutes * (TIMELABEL_WIDTH / timelineWidth);
+  const subMinutePxBuckets = startEndMinutes < timelineWidth;
+
+  for (const minutes of minuteRanges) {
+    if (minutes > timeLabelMinutes) {
+      return {
+        dateLabelFormat: getFormat({timeOnly: true, seconds: subMinutePxBuckets}),
+        elapsedMinutes: startEndMinutes,
+        timeMarkerInterval: minutes,
+        dateTimeProps: {timeOnly: true},
+      };
+    }
+  }
+
+  // Calculate days between each time label interval for larger time ranges
+  const timeLabelIntervalDays = Math.ceil(timeLabelMinutes / (60 * 24));
+  return {
+    dateLabelFormat: getFormat(),
+    elapsedMinutes: startEndMinutes,
+    timeMarkerInterval: timeLabelIntervalDays * 60 * 24,
+    dateTimeProps: {dateOnly: true},
+  };
+}

+ 2 - 2
static/app/views/monitors/utils/getTimeRangeFromEvent.tsx

@@ -2,7 +2,7 @@ import moment from 'moment';
 
 import {Event} from 'sentry/types';
 import {TimeWindow} from 'sentry/views/monitors/components/overviewTimeline/types';
-import {timeWindowConfig} from 'sentry/views/monitors/components/overviewTimeline/utils';
+import {resolutionElapsedMinutes} from 'sentry/views/monitors/components/overviewTimeline/utils';
 
 /**
  * Given a cron event, current time, and time window, attempt to return a
@@ -14,7 +14,7 @@ export function getTimeRangeFromEvent(
   now: Date,
   timeWindow: TimeWindow
 ): {end: Date; start: Date} {
-  const elapsedMinutes = timeWindowConfig[timeWindow].elapsedMinutes;
+  const elapsedMinutes = resolutionElapsedMinutes[timeWindow];
   let end = moment(event.dateReceived).add(elapsedMinutes / 2, 'minute');
   if (end > moment(now)) {
     end = moment(now);