Skip to content

Commit 6cadcb3

Browse files
authored
Timeseries chronological (#60058)
Previously we were breaking out of our total count aggregation because the timeseries we fetched was reverse chronological order. Sorting the timeseries will now ensure we're properly iterating through and aggregating values
1 parent 1f005d3 commit 6cadcb3

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed

src/sentry/api/endpoints/release_thresholds/release_threshold_status_index.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@ def is_error_count_healthy(ethreshold: EnrichedThreshold, timeseries: List[Dict[
378378
threshold_environment: str | None = (
379379
ethreshold["environment"]["name"] if ethreshold["environment"] else None
380380
)
381-
for i in timeseries:
381+
sorted_series = sorted(timeseries, key=lambda x: x["time"])
382+
for i in sorted_series:
382383
if parser.parse(i["time"]) > ethreshold["end"]:
383384
# timeseries are ordered chronologically
384385
# So if we're past our threshold.end, we can skip the rest

tests/sentry/api/endpoints/release_thresholds/test_release_threshold_status.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,3 +814,141 @@ def test_multiple_environments_within_timeseries(self):
814814
"window_in_seconds": 60, # NOTE: window_in_seconds only used to determine start/end. Not utilized in validation method
815815
}
816816
assert not is_error_count_healthy(ethreshold=threshold_unhealthy, timeseries=timeseries)
817+
818+
def test_unordered_timeseries(self):
819+
"""
820+
construct a timeseries with:
821+
- a single release
822+
- a single project
823+
- no environment
824+
- multiple timestamps both before and after our threshold window
825+
- all disorganized
826+
"""
827+
now = datetime.utcnow()
828+
timeseries = [
829+
{
830+
"release": self.release1.version,
831+
"project_id": self.project1.id,
832+
"time": (now - timedelta(minutes=3)).isoformat(),
833+
"environment": None,
834+
"count()": 1,
835+
},
836+
{
837+
"release": self.release1.version,
838+
"project_id": self.project1.id,
839+
"time": now.isoformat(),
840+
"environment": None,
841+
"count()": 1,
842+
},
843+
{
844+
"release": self.release1.version,
845+
"project_id": self.project1.id,
846+
"time": (now - timedelta(minutes=1)).isoformat(),
847+
"environment": None,
848+
"count()": 1,
849+
},
850+
{
851+
"release": self.release1.version,
852+
"project_id": self.project1.id,
853+
"time": (now - timedelta(minutes=2)).isoformat(),
854+
"environment": None,
855+
"count()": 1,
856+
},
857+
]
858+
859+
# current threshold within series
860+
current_threshold_healthy: EnrichedThreshold = {
861+
"date": now,
862+
"start": now - timedelta(minutes=1),
863+
"end": now,
864+
"environment": None,
865+
"is_healthy": False,
866+
"key": "",
867+
"project": serialize(self.project1),
868+
"project_id": self.project1.id,
869+
"project_slug": self.project1.slug,
870+
"release": self.release1.version,
871+
"threshold_type": ReleaseThresholdType.TOTAL_ERROR_COUNT,
872+
"trigger_type": TriggerType.OVER_STR,
873+
"value": 4, # error counts _not_ be over threshold value
874+
"window_in_seconds": 60, # NOTE: window_in_seconds only used to determine start/end. Not utilized in validation method
875+
}
876+
assert is_error_count_healthy(ethreshold=current_threshold_healthy, timeseries=timeseries)
877+
878+
# threshold equal to count
879+
threshold_at_limit_healthy: EnrichedThreshold = {
880+
"date": now,
881+
"start": now - timedelta(minutes=1),
882+
"end": now,
883+
"environment": None,
884+
"is_healthy": False,
885+
"key": "",
886+
"project": serialize(self.project1),
887+
"project_id": self.project1.id,
888+
"project_slug": self.project1.slug,
889+
"release": self.release1.version,
890+
"threshold_type": ReleaseThresholdType.TOTAL_ERROR_COUNT,
891+
"trigger_type": TriggerType.OVER_STR,
892+
"value": 1, # error counts equal to threshold limit value
893+
"window_in_seconds": 60, # NOTE: window_in_seconds only used to determine start/end. Not utilized in validation method
894+
}
895+
assert is_error_count_healthy(ethreshold=threshold_at_limit_healthy, timeseries=timeseries)
896+
897+
# past healthy threshold within series
898+
past_threshold_healthy: EnrichedThreshold = {
899+
"date": now,
900+
"start": now - timedelta(minutes=2),
901+
"end": now - timedelta(minutes=1),
902+
"environment": None,
903+
"is_healthy": False,
904+
"key": "",
905+
"project": serialize(self.project1),
906+
"project_id": self.project1.id,
907+
"project_slug": self.project1.slug,
908+
"release": self.release1.version,
909+
"threshold_type": ReleaseThresholdType.TOTAL_ERROR_COUNT,
910+
"trigger_type": TriggerType.OVER_STR,
911+
"value": 2,
912+
"window_in_seconds": 60, # NOTE: window_in_seconds only used to determine start/end. Not utilized in validation method
913+
}
914+
assert is_error_count_healthy(ethreshold=past_threshold_healthy, timeseries=timeseries)
915+
916+
# threshold within series but trigger is under
917+
threshold_under_unhealthy: EnrichedThreshold = {
918+
"date": now,
919+
"start": now - timedelta(minutes=1),
920+
"end": now,
921+
"environment": None,
922+
"is_healthy": False,
923+
"key": "",
924+
"project": serialize(self.project1),
925+
"project_id": self.project1.id,
926+
"project_slug": self.project1.slug,
927+
"release": self.release1.version,
928+
"threshold_type": ReleaseThresholdType.TOTAL_ERROR_COUNT,
929+
"trigger_type": TriggerType.UNDER_STR,
930+
"value": 4,
931+
"window_in_seconds": 60, # NOTE: window_in_seconds only used to determine start/end. Not utilized in validation method
932+
}
933+
assert not is_error_count_healthy(
934+
ethreshold=threshold_under_unhealthy, timeseries=timeseries
935+
)
936+
937+
# threshold within series but end is in future
938+
threshold_unfinished: EnrichedThreshold = {
939+
"date": now,
940+
"start": now - timedelta(minutes=1),
941+
"end": now + timedelta(minutes=5),
942+
"environment": None,
943+
"is_healthy": False,
944+
"key": "",
945+
"project": serialize(self.project1),
946+
"project_id": self.project1.id,
947+
"project_slug": self.project1.slug,
948+
"release": self.release1.version,
949+
"threshold_type": ReleaseThresholdType.TOTAL_ERROR_COUNT,
950+
"trigger_type": TriggerType.OVER_STR,
951+
"value": 4,
952+
"window_in_seconds": 60, # NOTE: window_in_seconds only used to determine start/end. Not utilized in validation method
953+
}
954+
assert is_error_count_healthy(ethreshold=threshold_unfinished, timeseries=timeseries)

0 commit comments

Comments
 (0)