Skip to content

feat(aci): Only update open periods if the initial one exists #90079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion src/sentry/api/helpers/group_index/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@
from sentry.issues.update_inbox import update_inbox
from sentry.models.activity import Activity, ActivityIntegration
from sentry.models.commit import Commit
from sentry.models.group import STATUS_UPDATE_CHOICES, Group, GroupStatus, update_group_open_period
from sentry.models.group import STATUS_UPDATE_CHOICES, Group, GroupStatus
from sentry.models.groupassignee import GroupAssignee
from sentry.models.groupbookmark import GroupBookmark
from sentry.models.grouphash import GroupHash
from sentry.models.grouphistory import record_group_history_from_activity_type
from sentry.models.groupinbox import GroupInboxRemoveAction, remove_group_from_inbox
from sentry.models.grouplink import GroupLink
from sentry.models.groupopenperiod import update_group_open_period
from sentry.models.groupresolution import GroupResolution
from sentry.models.groupseen import GroupSeen
from sentry.models.groupshare import GroupShare
Expand Down
6 changes: 4 additions & 2 deletions src/sentry/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
from sentry.models.grouphash import GroupHash
from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
from sentry.models.grouplink import GroupLink
from sentry.models.groupopenperiod import GroupOpenPeriod
from sentry.models.groupopenperiod import GroupOpenPeriod, has_initial_open_period
from sentry.models.grouprelease import GroupRelease
from sentry.models.groupresolution import GroupResolution
from sentry.models.organization import Organization
Expand Down Expand Up @@ -1715,7 +1715,9 @@ def _handle_regression(group: Group, event: BaseEvent, release: Release | None)
kick_off_status_syncs.apply_async(
kwargs={"project_id": group.project_id, "group_id": group.id}
)
if features.has("organizations:issue-open-periods", group.project.organization):
if features.has(
"organizations:issue-open-periods", group.project.organization
) and has_initial_open_period(group):
Comment on lines +1718 to +1720
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the larger diff - calling out the logic changes

GroupOpenPeriod.objects.create(
group=group,
project_id=group.project_id,
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/issues/endpoints/group_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
from sentry.issues.grouptype import GroupCategory
from sentry.models.activity import Activity
from sentry.models.eventattachment import EventAttachment
from sentry.models.group import Group, get_open_periods_for_group
from sentry.models.group import Group
from sentry.models.groupinbox import get_inbox_details
from sentry.models.grouplink import GroupLink
from sentry.models.groupopenperiod import get_open_periods_for_group
from sentry.models.groupowner import get_owner_details
from sentry.models.groupseen import GroupSeen
from sentry.models.groupsubscription import GroupSubscriptionManager
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/issues/endpoints/group_open_periods.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.utils import get_date_range_from_params
from sentry.exceptions import InvalidParams
from sentry.models.group import get_open_periods_for_group
from sentry.models.groupopenperiod import get_open_periods_for_group

if TYPE_CHECKING:
from sentry.models.group import Group
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/issues/status_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from sentry.issues.ignored import IGNORED_CONDITION_FIELDS
from sentry.issues.ongoing import TRANSITION_AFTER_DAYS
from sentry.models.activity import Activity
from sentry.models.group import Group, GroupStatus, update_group_open_period
from sentry.models.group import Group, GroupStatus
from sentry.models.grouphistory import record_group_history_from_activity_type
from sentry.models.groupopenperiod import update_group_open_period
from sentry.models.groupsubscription import GroupSubscription
from sentry.models.project import Project
from sentry.notifications.types import GroupSubscriptionReason
Expand Down
188 changes: 2 additions & 186 deletions src/sentry/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from django.utils.translation import gettext_lazy as _
from snuba_sdk import Column, Condition, Op

from sentry import eventstore, eventtypes, features, options, tagstore
from sentry import eventstore, eventtypes, options, tagstore
from sentry.backup.scopes import RelocationScope
from sentry.constants import DEFAULT_LOGGER_NAME, LOG_LEVELS, MAX_CULPRIT_LENGTH
from sentry.db.models import (
Expand All @@ -42,10 +42,8 @@
PriorityChangeReason,
get_priority_for_ongoing_group,
)
from sentry.models.activity import Activity
from sentry.models.commit import Commit
from sentry.models.grouphistory import record_group_history, record_group_history_from_activity_type
from sentry.models.groupopenperiod import GroupOpenPeriod
from sentry.models.organization import Organization
from sentry.snuba.dataset import Dataset
from sentry.snuba.referrer import Referrer
Expand All @@ -62,7 +60,6 @@
from sentry.utils.strings import strip, truncatechars

if TYPE_CHECKING:
from sentry.incidents.utils.metric_issue_poc import OpenPeriod
from sentry.integrations.services.integration import RpcIntegration
from sentry.models.environment import Environment
from sentry.models.team import Team
Expand Down Expand Up @@ -448,6 +445,7 @@ def update_group_status(
) -> None:
"""For each groups, update status to `status` and create an Activity."""
from sentry.models.activity import Activity
from sentry.models.groupopenperiod import update_group_open_period

modified_groups_list = []
selected_groups = Group.objects.filter(id__in=[g.id for g in groups]).exclude(
Expand Down Expand Up @@ -1063,185 +1061,3 @@ def pre_save_group_default_substatus(instance, sender, *args, **kwargs):
"No substatus allowed for group",
extra={"status": instance.status, "substatus": instance.substatus},
)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of this is moved to groupopenperiod.py


def get_last_checked_for_open_period(group: Group) -> datetime:
from sentry.incidents.models.alert_rule import AlertRule
from sentry.issues.grouptype import MetricIssuePOC

event = group.get_latest_event()
last_checked = group.last_seen
if event and group.type == MetricIssuePOC.type_id:
alert_rule_id = event.data.get("contexts", {}).get("metric_alert", {}).get("alert_rule_id")
if alert_rule_id:
try:
alert_rule = AlertRule.objects.get(id=alert_rule_id)
now = timezone.now()
last_checked = now - timedelta(seconds=alert_rule.snuba_query.time_window)
except AlertRule.DoesNotExist:
pass

return last_checked


def get_open_periods_for_group(
group: Group,
query_start: datetime | None = None,
query_end: datetime | None = None,
offset: int | None = None,
limit: int | None = None,
) -> list[OpenPeriod]:
from sentry.incidents.utils.metric_issue_poc import OpenPeriod
from sentry.models.groupopenperiod import GroupOpenPeriod

if not features.has("organizations:issue-open-periods", group.organization):
return []

# Try to get open periods from the GroupOpenPeriod table first
group_open_periods = GroupOpenPeriod.objects.filter(group=group)
if group_open_periods.exists() and query_start:
group_open_periods = group_open_periods.filter(
date_started__gte=query_start, date_ended__lte=query_end, id__gte=offset or 0
).order_by("-date_started")[:limit]

return [
OpenPeriod(
start=period.date_started,
end=period.date_ended,
duration=period.date_ended - period.date_started if period.date_ended else None,
is_open=period.date_ended is None,
last_checked=get_last_checked_for_open_period(group),
)
for period in group_open_periods
]

# If there are no open periods in the table, we need to calculate them
# from the activity log.
# TODO(snigdha): This is temporary until we have backfilled the GroupOpenPeriod table

if query_start is None or query_end is None:
query_start = timezone.now() - timedelta(days=90)
query_end = timezone.now()

query_limit = limit * 2 if limit else None
# Filter to REGRESSION and RESOLVED activties to find the bounds of each open period.
# The only UNRESOLVED activity we would care about is the first UNRESOLVED activity for the group creation,
# but we don't create an entry for that .
activities = Activity.objects.filter(
group=group,
type__in=[ActivityType.SET_REGRESSION.value, ActivityType.SET_RESOLVED.value],
datetime__gte=query_start,
datetime__lte=query_end,
).order_by("-datetime")[:query_limit]

open_periods = []
start: datetime | None = None
end: datetime | None = None
last_checked = get_last_checked_for_open_period(group)

# Handle currently open period
if group.status == GroupStatus.UNRESOLVED and len(activities) > 0:
open_periods.append(
OpenPeriod(
start=activities[0].datetime,
end=None,
duration=None,
is_open=True,
last_checked=last_checked,
)
)
activities = activities[1:]

for activity in activities:
if activity.type == ActivityType.SET_RESOLVED.value:
end = activity.datetime
elif activity.type == ActivityType.SET_REGRESSION.value:
start = activity.datetime
if end is not None:
open_periods.append(
OpenPeriod(
start=start,
end=end,
duration=end - start,
is_open=False,
last_checked=end,
)
)
end = None

# Add the very first open period, which has no UNRESOLVED activity for the group creation
open_periods.append(
OpenPeriod(
start=group.first_seen,
end=end if end else None,
duration=end - group.first_seen if end else None,
is_open=False if end else True,
last_checked=end if end else last_checked,
)
)

if offset and limit:
return open_periods[offset : offset + limit]

if limit:
return open_periods[:limit]

return open_periods


def update_group_open_period(
group: Group,
new_status: int,
activity: Activity | None,
should_reopen_open_period: bool,
) -> None:
"""
Update an existing open period when the group is resolved or unresolved.

On resolution, we set the date_ended to the resolution time and link the activity to the open period.
On unresolved, we clear the date_ended and resolution_activity fields. This is only done if the group
is unresolved manually without a regression. If the group is unresolved due to a regression, the
open periods will be updated during ingestion.
"""
if not features.has("organizations:issue-open-periods", group.project.organization):
return

if new_status not in (GroupStatus.RESOLVED, GroupStatus.UNRESOLVED):
return

find_open = new_status != GroupStatus.UNRESOLVED
open_period = (
GroupOpenPeriod.objects.filter(group=group, date_ended__isnull=find_open)
.order_by("-date_started")
.first()
)
if not open_period:
logger.error(
"Unable to update open period, no open period found",
extra={"group_id": group.id},
)
return

if new_status == GroupStatus.RESOLVED:
if activity is None:
logger.warning(
"Missing activity for group resolution, querying for it",
extra={"group_id": group.id},
)
activity = (
Activity.objects.filter(
group=group,
type=ActivityType.SET_RESOLVED.value,
)
.order_by("-datetime")
.first()
)

end_time = group.resolved_at or (activity.datetime if activity else None)
open_period.update(
date_ended=end_time,
resolution_activity=activity,
user_id=activity.user_id if activity else None,
)
elif new_status == GroupStatus.UNRESOLVED and should_reopen_open_period:
open_period.update(date_ended=None, resolution_activity=None, user_id=None)
Loading
Loading