Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/sentry/models/groupopenperiod.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,21 +144,41 @@ def get_open_periods_for_group(
query_end: datetime | None = None,
limit: int | None = None,
) -> BaseQuerySet[GroupOpenPeriod]:
"""
Get open periods for a group that overlap with the query time range.

To overlap with [query_start, query_end], an open period must:
1. Start before the query ends
2. End after the query starts (or still be open)

This covers all overlap cases:
- Period starts before query and ends within query range
- Period starts before query and ends after query (open period spans entire query range)
- Period starts within query and ends within query (open period completely inside query range)
- Period starts within query and ends after query
- Period starts before query and is still open
- Period starts within query and is still open
"""
if not should_create_open_periods(group.type):
return GroupOpenPeriod.objects.none()

if not query_start:
# use whichever date is more recent to reduce the query range. first_seen could be > 90 days ago
query_start = max(group.first_seen, timezone.now() - timedelta(days=90))
if not query_end:
query_end = timezone.now()

started_before_query_ends = Q(date_started__lte=query_end)
ended_after_query_starts = Q(date_ended__gte=query_start)
still_open = Q(date_ended__isnull=True)

group_open_periods = GroupOpenPeriod.objects.filter(
group=group,
date_started__gte=query_start,
).order_by("-date_started")
if query_end:
group_open_periods = group_open_periods.filter(
Q(date_ended__lte=query_end) | Q(date_ended__isnull=True)
group_open_periods = (
GroupOpenPeriod.objects.filter(
group=group,
)
.filter(started_before_query_ends & (ended_after_query_starts | still_open))
.order_by("-date_started")
)

return group_open_periods[:limit]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import timedelta

from django.utils import timezone

from sentry.incidents.grouptype import MetricIssue
Expand Down Expand Up @@ -280,3 +282,127 @@ def test_open_periods_limit(self) -> None:
assert resp["start"] == unresolved_time
assert resp["end"] == second_resolved_time
assert resp["isOpen"] is False

def test_get_open_periods_time_range_starts_after_query_start(self) -> None:
"""Test that open periods starting after query_start and ending after query_end are included."""
base_time = timezone.now() - timedelta(days=10)
GroupOpenPeriod.objects.filter(group=self.group).delete()

# Open period: Day 2 to Day 7
open_period = GroupOpenPeriod.objects.create(
group=self.group,
project=self.group.project,
date_started=base_time + timedelta(days=2),
date_ended=base_time + timedelta(days=7),
)

# Query range: Day 0 to Day 5
query_start = base_time.isoformat()
query_end = (base_time + timedelta(days=5)).isoformat()

response = self.get_success_response(
*self.get_url_args(),
qs_params={
"groupId": self.group.id,
"start": query_start,
"end": query_end,
},
)

assert len(response.data) == 1
resp = response.data[0]
assert resp["id"] == str(open_period.id)

def test_get_open_periods_time_range_starts_before_ends_within(self) -> None:
"""Test that open periods starting before query_start and ending before query_end are included."""

base_time = timezone.now() - timedelta(days=10)

GroupOpenPeriod.objects.filter(group=self.group).delete()

# Open period: Day 0 to Day 3 (ends within range)
open_period = GroupOpenPeriod.objects.create(
group=self.group,
project=self.group.project,
date_started=base_time,
date_ended=base_time + timedelta(days=3),
)

# Query range: Day 2 to Day 7
query_start = (base_time + timedelta(days=2)).isoformat()
query_end = (base_time + timedelta(days=7)).isoformat()

response = self.get_success_response(
*self.get_url_args(),
qs_params={
"groupId": self.group.id,
"start": query_start,
"end": query_end,
},
)

assert len(response.data) == 1
resp = response.data[0]
assert resp["id"] == str(open_period.id)

def test_get_open_periods_time_range_starts_before_still_ongoing(self) -> None:
"""Test that open periods starting before query_start and still ongoing (date_ended=None) are included."""

base_time = timezone.now() - timedelta(days=10)

GroupOpenPeriod.objects.filter(group=self.group).delete()

# Open period: Day 1 to ongoing
open_period = GroupOpenPeriod.objects.create(
group=self.group,
project=self.group.project,
date_started=base_time + timedelta(days=1),
date_ended=None,
)

# Query range: Day 0 to Day 7
query_start = base_time.isoformat()
query_end = (base_time + timedelta(days=7)).isoformat()

response = self.get_success_response(
*self.get_url_args(),
qs_params={
"groupId": self.group.id,
"start": query_start,
"end": query_end,
},
)

assert len(response.data) == 1
resp = response.data[0]
assert resp["id"] == str(open_period.id)

def test_get_open_periods_none_in_range(self) -> None:
"""Test that open periods outside the query range are not included."""

base_time = timezone.now() - timedelta(days=10)

GroupOpenPeriod.objects.filter(group=self.group).delete()

# Open period: Day 0 to Day 1 (starts + ends before query range)
GroupOpenPeriod.objects.create(
group=self.group,
project=self.group.project,
date_started=base_time,
date_ended=base_time + timedelta(days=1),
)

# Query range: Day 2 to Day 7
query_start = (base_time + timedelta(days=2)).isoformat()
query_end = (base_time + timedelta(days=7)).isoformat()

response = self.get_success_response(
*self.get_url_args(),
qs_params={
"groupId": self.group.id,
"start": query_start,
"end": query_end,
},
)

assert len(response.data) == 0
Loading