diff --git a/src/sentry/api/utils.py b/src/sentry/api/utils.py index 33b9cbd6c1339d..25ca63b7235fcd 100644 --- a/src/sentry/api/utils.py +++ b/src/sentry/api/utils.py @@ -9,19 +9,36 @@ MIN_STATS_PERIOD = timedelta(hours=1) MAX_STATS_PERIOD = timedelta(days=90) +# make sure to update this message if you are changing the min/max period +INVALID_PERIOD_ERROR = 'Time window must be greater than an hour and less than or equal to 90 days' class InvalidParams(Exception): pass +def get_datetime_from_stats_period(stats_period, now=None): + if now is None: + now = timezone.now() + stats_period = parse_stats_period(stats_period) + if stats_period is None: + raise InvalidParams('Invalid statsPeriod') + return now - stats_period + + def get_date_range_from_params(params, optional=False): """ Gets a date range from standard date range params we pass to the api. + If `statsPeriod` is passed then convert to a time delta and make sure it fits within our min/max period length. Values are in the format , where period type is one of `s` (seconds), `m` (minutes), `h` (hours) or `d` (days). + + Similarly, `statsPeriodStart` and `statsPeriodEnd` allow for selecting a + relative range, for example: 15 days ago through 8 days ago. This uses the same + format as `statsPeriod` + :param params: If `start` end `end` are passed, validate them, convert to `datetime` and returns them if valid. @@ -35,11 +52,18 @@ def get_date_range_from_params(params, optional=False): start = now - MAX_STATS_PERIOD stats_period = params.get('statsPeriod') + stats_period_start = params.get('statsPeriodStart') + stats_period_end = params.get('statsPeriodEnd') + if stats_period is not None: - stats_period = parse_stats_period(stats_period) - if stats_period is None or stats_period < MIN_STATS_PERIOD or stats_period >= MAX_STATS_PERIOD: - raise InvalidParams('Invalid statsPeriod') - start = now - stats_period + start = get_datetime_from_stats_period(stats_period, now) + + elif stats_period_start or stats_period_end: + if not all([stats_period_start, stats_period_end]): + raise InvalidParams('statsPeriodStart and statsPeriodEnd are both required') + start = get_datetime_from_stats_period(stats_period_start, now) + end = get_datetime_from_stats_period(stats_period_end, now) + elif params.get('start') or params.get('end'): if not all([params.get('start'), params.get('end')]): raise InvalidParams('start and end are both required') @@ -48,9 +72,14 @@ def get_date_range_from_params(params, optional=False): end = parse_datetime_string(params['end']) except InvalidQuery as exc: raise InvalidParams(exc.message) - if start > end: - raise InvalidParams('start must be before end') elif optional: return None, None + if start > end: + raise InvalidParams('start must be before end') + + delta = end - start + if delta < MIN_STATS_PERIOD or delta > MAX_STATS_PERIOD: + raise InvalidParams(INVALID_PERIOD_ERROR) + return start, end diff --git a/tests/sentry/api/test_utils.py b/tests/sentry/api/test_utils.py index b9a33c1294378a..97e13116804082 100644 --- a/tests/sentry/api/test_utils.py +++ b/tests/sentry/api/test_utils.py @@ -51,3 +51,21 @@ def test_no_params(self): start, end = get_date_range_from_params({}, optional=True) assert start is None assert end is None + + @freeze_time('2018-12-11 03:21:34') + def test_relative_date_range(self): + start, end = get_date_range_from_params({ + 'statsPeriodStart': '14d', + 'statsPeriodEnd': '7d', + }) + + assert start == datetime.datetime(2018, 11, 27, 3, 21, 34, tzinfo=timezone.utc) + assert end == datetime.datetime(2018, 12, 4, 3, 21, 34, tzinfo=timezone.utc) + + @freeze_time('2018-12-11 03:21:34') + def test_relative_date_range_incomplete(self): + + with self.assertRaises(InvalidParams): + start, end = get_date_range_from_params({ + 'statsPeriodStart': '14d', + })