From 38eb8d2e50849ca52e69a953f320a6a54f2738c4 Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Thu, 2 Jul 2026 15:43:06 +0200 Subject: [PATCH] Support `SOURCE_DATE_EPOCH` to use as the date to check expiry against --- CHANGELOG.md | 2 ++ glean_parser/util.py | 13 +++++++++- tests/test_parser.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 205797ec..5a426a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Allow to overwrite date to check expiry against using the `SOURCE_DATE_EPOCH` environment variable ([bug 2052175](https://bugzilla.mozilla.org/show_bug.cgi?id=2052175)) + ## 20.0.1 - Ensure `in_session` is present for all metrics in the object model, like the other CommonMetricData args ([bug 2046193](https://bugzilla.mozilla.org/show_bug.cgi?id=2046193)). diff --git a/glean_parser/util.py b/glean_parser/util.py index c7efb72d..4f35b615 100644 --- a/glean_parser/util.py +++ b/glean_parser/util.py @@ -5,6 +5,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import datetime +import time +import os import functools import json from pathlib import Path @@ -412,6 +414,15 @@ def parse_expiration_version(expires: str) -> int: ) +def now_epoch(): + try: + epoch_ts = int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) + except ValueError: + raise ValueError("Invalid SOURCE_DATE_EPOCH") + + return datetime.datetime.fromtimestamp(epoch_ts, tz=datetime.timezone.utc) + + def is_expired(expires: str, major_version: Optional[int] = None) -> bool: """ Parses the `expires` field in a metric or ping and returns whether @@ -425,7 +436,7 @@ def is_expired(expires: str, major_version: Optional[int] = None) -> bool: return parse_expiration_version(expires) <= major_version else: date = parse_expiration_date(expires) - return date <= datetime.datetime.now(datetime.timezone.utc).date() + return date <= now_epoch().date() def validate_expires(expires: str, major_version: Optional[int] = None) -> None: diff --git a/tests/test_parser.py b/tests/test_parser.py index b0205eac..51e78e8e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -7,6 +7,8 @@ import json import re import textwrap +import os +import datetime import pytest @@ -1450,3 +1452,62 @@ def test_forbid_non_ext_non_object_attribution_distribution(): "Extended attribution/distribution metrics must be of type 'object'" in errors[1] ) + + +def test_expire_epoch_can_be_overriden(): + metrics = [ + { + "category": { + "metric": { + "type": "boolean", + "expires": "2023-12-31", # definitely in the past + }, + } + } + ] + metrics = [util.add_required(x) for x in metrics] + + all_metrics = parser.parse_objects(metrics) + errors = list(all_metrics) + assert len(errors) == 0 + assert all_metrics.value["category"]["metric"].disabled is True + + # A date definitely before the above expiry. + dt = datetime.datetime.strptime("2020-01-01", "%Y-%m-%d") + os.environ["SOURCE_DATE_EPOCH"] = str(int(dt.timestamp())) + + all_metrics = parser.parse_objects(metrics) + errors = list(all_metrics) + assert len(errors) == 0 + assert all_metrics.value["category"]["metric"].disabled is False + + del os.environ["SOURCE_DATE_EPOCH"] + + +@pytest.mark.parametrize( + "invalid_epoch", + [ + "not-a-date", + "2020-01-01", + "", + ], +) +def test_overriden_expire_epoch_must_be_valid(invalid_epoch): + metrics = [ + { + "category": { + "metric": { + "type": "boolean", + "expires": "2023-12-31", # definitely in the past + }, + } + } + ] + metrics = [util.add_required(x) for x in metrics] + + with pytest.raises(ValueError, match="Invalid SOURCE_DATE_EPOCH"): + os.environ["SOURCE_DATE_EPOCH"] = invalid_epoch + all_metrics = parser.parse_objects(metrics) + list(all_metrics) + + del os.environ["SOURCE_DATE_EPOCH"]