Skip to content

Refactor monitor failure handling into dedicated modules - #2

Open
smb060606 wants to merge 1 commit into
monitor-incident-refactor-beforefrom
monitor-incident-refactor-after
Open

Refactor monitor failure handling into dedicated modules#2
smb060606 wants to merge 1 commit into
monitor-incident-refactor-beforefrom
monitor-incident-refactor-after

Conversation

@smb060606

@smb060606 smb060606 commented Dec 12, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Implemented threshold-based incident creation for monitor failures, allowing systems to evaluate failure patterns and automatically generate incidents based on configurable thresholds.
  • Refactor

    • Reorganized monitor failure handling and incident occurrence generation logic for improved reliability and maintainability.

✏️ Tip: You can customize this high-level summary in your review settings.

…0528)

Since we'll be doing more with issue occurrences split out the concept
of incidents into it's own logic module, as well as incident_occurrence
into it's own module

Part of GH-80527
@coderabbitai

coderabbitai Bot commented Dec 12, 2025

Copy link
Copy Markdown

Walkthrough

The changes refactor monitor failure handling by extracting incident creation and occurrence generation logic into dedicated modules. A new incident_occurrence.py module handles Kafka-based event publishing for monitor failures, while incidents.py implements threshold-based incident detection. The mark_failed.py module is simplified to delegate to the new incident threshold logic. A new SimpleCheckIn TypedDict is added to support the refactored code.

Changes

Cohort / File(s) Summary
New incident occurrence module
src/sentry/monitors/logic/incident_occurrence.py
Implements Kafka-based incident occurrence generation with create_incident_occurrence, get_failure_reason, and get_monitor_environment_context functions. Includes localized failure status mappings via HUMAN_FAILURE_STATUS_MAP and SINGULAR_HUMAN_FAILURE_MAP for aggregating failure narratives.
New incident threshold evaluation module
src/sentry/monitors/logic/incidents.py
Adds try_incident_threshold function to evaluate monitor failure counts against thresholds and create MonitorIncident instances. Handles prior check-in retrieval, status transitions, incident creation, occurrence generation, and signal emission for failed environments.
Refactored mark_failed module
src/sentry/monitors/logic/mark_failed.py
Simplifies mark_failed function to delegate to try_incident_threshold, removing inline incident creation, occurrence generation, and helper functions previously contained in the module. Updates imports to reflect reduced responsibilities.
Type definitions
src/sentry/monitors/types.py
Adds SimpleCheckIn TypedDict with fields id, date_added, and status to represent minimal check-in data structures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • incidents.py: Threshold evaluation logic requires careful review of check-in retrieval patterns, status transitions (OK/ACTIVE → ERROR), and incident state management to ensure correctness across edge cases (threshold == 1, query optimization paths).
  • incident_occurrence.py: Event data construction, fingerprint generation, timestamp handling, and Kafka publishing integration need validation for proper event formatting and downstream processing.
  • mark_failed.py: Verify that delegation to try_incident_threshold preserves all existing behavior; confirm parameter mapping and return value handling match original workflow.
  • Integration points: Ensure signals are emitted correctly, monitor/environment muting logic is respected, and cross-module dependencies are cohesive.

Poem

🐰 Incidents hop through brand new modules,
Thresholds checked with Kafka tuttle-tuttle,
Old mark_failed delegates with grace,
SimpleCheckIn takes its rightful place,
Monitor failures now refactored neat! ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title '@coderabbitai' is not descriptive of the actual changes and fails to communicate the pull request's primary purpose. Replace with a clear title summarizing the main change, such as 'Refactor monitor incident logic to centralize threshold evaluation and occurrence creation' or similar.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch monitor-incident-refactor-after

Comment @coderabbitai help to get the list of available commands and usage tips.

@smb060606

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Dec 12, 2025

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Refactor monitor failure handling into dedicated modules Dec 12, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/sentry/monitors/logic/incidents.py (1)

52-53: Minor: Prefer generator expression inside any().

Using a generator expression allows any() to short-circuit without building the full list:

-            if any([checkin["status"] == CheckInStatus.OK for checkin in previous_checkins]):
+            if any(checkin["status"] == CheckInStatus.OK for checkin in previous_checkins):
src/sentry/monitors/logic/incident_occurrence.py (1)

130-156: Address static analysis hint and minor typo.

  1. Line 132: Typo "humam" → "human" in docstring
  2. Line 146: Per Ruff RUF015, prefer next(iter(...)) over single-element slice
 def get_failure_reason(failed_checkins: Sequence[SimpleCheckIn]):
     """
-    Builds a humam readible string from a list of failed check-ins.
+    Builds a human readable string from a list of failed check-ins.

     "3 missed check-ins detected"
     "2 missed check-ins, 1 timeout check-in and 1 error check-in were detected"
     "A failed check-in was detected"
     """

     status_counts = Counter(
         checkin["status"]
         for checkin in failed_checkins
-        if checkin["status"] in HUMAN_FAILURE_STATUS_MAP.keys()
+        if checkin["status"] in HUMAN_FAILURE_STATUS_MAP
     )

     if sum(status_counts.values()) == 1:
-        return SINGULAR_HUMAN_FAILURE_MAP[list(status_counts.keys())[0]]
+        return SINGULAR_HUMAN_FAILURE_MAP[next(iter(status_counts.keys()))]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5fc9f67 and c3b32a7.

📒 Files selected for processing (4)
  • src/sentry/monitors/logic/incident_occurrence.py (1 hunks)
  • src/sentry/monitors/logic/incidents.py (1 hunks)
  • src/sentry/monitors/logic/mark_failed.py (2 hunks)
  • src/sentry/monitors/types.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/sentry/monitors/types.py (2)
src/sentry/sentry_apps/services/app/model.py (1)
  • id (112-112)
src/sentry/eventstore/models.py (1)
  • datetime (112-120)
src/sentry/monitors/logic/incidents.py (2)
src/sentry/monitors/models.py (4)
  • CheckInStatus (124-176)
  • MonitorCheckIn (472-582)
  • MonitorIncident (747-795)
  • MonitorStatus (80-121)
src/sentry/monitors/types.py (1)
  • SimpleCheckIn (105-112)
src/sentry/monitors/logic/mark_failed.py (2)
src/sentry/monitors/logic/incidents.py (1)
  • try_incident_threshold (14-104)
src/sentry/monitors/models.py (3)
  • CheckInStatus (124-176)
  • MonitorCheckIn (472-582)
  • MonitorEnvironment (644-724)
src/sentry/monitors/logic/incident_occurrence.py (4)
src/sentry/monitors/models.py (7)
  • timezone (340-341)
  • CheckInStatus (124-176)
  • MonitorCheckIn (472-582)
  • MonitorEnvironment (644-724)
  • MonitorIncident (747-795)
  • owner_actor (324-325)
  • get_schedule_type_display (343-344)
src/sentry/issues/grouptype.py (1)
  • MonitorIncidentType (528-536)
src/sentry/monitors/types.py (1)
  • SimpleCheckIn (105-112)
src/sentry/issues/producer.py (2)
  • PayloadType (25-34)
  • produce_occurrence_to_kafka (50-90)
🪛 Ruff (0.14.8)
src/sentry/monitors/logic/incident_occurrence.py

146-146: Prefer next(iter(status_counts.keys())) over single element slice

Replace with next(iter(status_counts.keys()))

(RUF015)

🔇 Additional comments (4)
src/sentry/monitors/types.py (1)

105-113: LGTM!

Clean, well-documented TypedDict that provides a minimal representation for check-in data. The fields align correctly with the usage in incidents.py where it's populated from both direct model access and .values() queries.

src/sentry/monitors/logic/mark_failed.py (1)

14-79: LGTM - Clean separation of concerns.

The refactoring properly separates responsibilities:

  • mark_failed handles timing updates and MonitorEnvironment state
  • try_incident_threshold handles incident creation logic

The conditional update pattern at lines 52-63 correctly prevents race conditions with newer check-ins.

src/sentry/monitors/logic/incidents.py (1)

89-104: LGTM - Occurrence creation and signal emission.

The logic correctly:

  • Respects mute settings on both monitor and environment
  • Re-fetches full MonitorCheckIn objects needed by create_incident_occurrence
  • Sends the signal regardless of mute status (preserving original behavior)
src/sentry/monitors/logic/incident_occurrence.py (1)

28-113: LGTM - Well-structured occurrence generation.

The function properly:

  • Guards against missing monitor_env
  • Constructs comprehensive evidence display
  • Handles optional trace_id and received timestamp
  • Uses lazy imports to avoid circular dependencies

Comment on lines +159 to +171
def get_monitor_environment_context(monitor_environment: MonitorEnvironment):
config = monitor_environment.monitor.config.copy()
if "schedule_type" in config:
config["schedule_type"] = monitor_environment.monitor.get_schedule_type_display()

return {
"id": str(monitor_environment.monitor.guid),
"slug": str(monitor_environment.monitor.slug),
"name": monitor_environment.monitor.name,
"config": monitor_environment.monitor.config,
"status": monitor_environment.get_status_display(),
"type": monitor_environment.monitor.get_type_display(),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: Modified config copy is not used in the returned dictionary.

The function creates a modified copy of config with a human-readable schedule_type, but line 168 returns the original monitor_environment.monitor.config instead of the modified config variable.

     return {
         "id": str(monitor_environment.monitor.guid),
         "slug": str(monitor_environment.monitor.slug),
         "name": monitor_environment.monitor.name,
-        "config": monitor_environment.monitor.config,
+        "config": config,
         "status": monitor_environment.get_status_display(),
         "type": monitor_environment.monitor.get_type_display(),
     }
🤖 Prompt for AI Agents
In src/sentry/monitors/logic/incident_occurrence.py around lines 159 to 171, the
function builds a modified copy named `config` (with a human-readable
`schedule_type`) but then returns the original
`monitor_environment.monitor.config`; change the returned dictionary to use the
modified `config` variable instead of `monitor_environment.monitor.config` so
the human-readable schedule_type is included in the output.

Comment on lines +36 to +59
else:
previous_checkins = cast(
list[SimpleCheckIn],
# Using .values for performance reasons
MonitorCheckIn.objects.filter(
monitor_environment=monitor_env, date_added__lte=failed_checkin.date_added
)
.order_by("-date_added")
.values("id", "date_added", "status"),
)

# reverse the list after slicing in order to start with oldest check-in
previous_checkins = list(reversed(previous_checkins[:failure_issue_threshold]))

# If we have any successful check-ins within the threshold of
# commits we have NOT reached an incident state
if any([checkin["status"] == CheckInStatus.OK for checkin in previous_checkins]):
return False

# change monitor status + update fingerprint timestamp
monitor_env.status = MonitorStatus.ERROR
monitor_env.save(update_fields=("status",))

starting_checkin = previous_checkins[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential IndexError when threshold exceeds available check-ins.

When failure_issue_threshold > 1 and the monitor environment has no prior check-ins (e.g., a brand new monitor that immediately fails), previous_checkins will be an empty list after slicing. The any() check on line 52 returns False for an empty list, so the function won't return early, and line 59 will raise an IndexError.

Consider adding a guard for this edge case:

             # reverse the list after slicing in order to start with oldest check-in
             previous_checkins = list(reversed(previous_checkins[:failure_issue_threshold]))

+            # Not enough check-ins to evaluate threshold
+            if not previous_checkins:
+                return False
+
             # If we have any successful check-ins within the threshold of
             # commits we have NOT reached an incident state
             if any([checkin["status"] == CheckInStatus.OK for checkin in previous_checkins]):
                 return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else:
previous_checkins = cast(
list[SimpleCheckIn],
# Using .values for performance reasons
MonitorCheckIn.objects.filter(
monitor_environment=monitor_env, date_added__lte=failed_checkin.date_added
)
.order_by("-date_added")
.values("id", "date_added", "status"),
)
# reverse the list after slicing in order to start with oldest check-in
previous_checkins = list(reversed(previous_checkins[:failure_issue_threshold]))
# If we have any successful check-ins within the threshold of
# commits we have NOT reached an incident state
if any([checkin["status"] == CheckInStatus.OK for checkin in previous_checkins]):
return False
# change monitor status + update fingerprint timestamp
monitor_env.status = MonitorStatus.ERROR
monitor_env.save(update_fields=("status",))
starting_checkin = previous_checkins[0]
else:
previous_checkins = cast(
list[SimpleCheckIn],
# Using .values for performance reasons
MonitorCheckIn.objects.filter(
monitor_environment=monitor_env, date_added__lte=failed_checkin.date_added
)
.order_by("-date_added")
.values("id", "date_added", "status"),
)
# reverse the list after slicing in order to start with oldest check-in
previous_checkins = list(reversed(previous_checkins[:failure_issue_threshold]))
# Not enough check-ins to evaluate threshold
if not previous_checkins:
return False
# If we have any successful check-ins within the threshold of
# commits we have NOT reached an incident state
if any([checkin["status"] == CheckInStatus.OK for checkin in previous_checkins]):
return False
# change monitor status + update fingerprint timestamp
monitor_env.status = MonitorStatus.ERROR
monitor_env.save(update_fields=("status",))
starting_checkin = previous_checkins[0]
🤖 Prompt for AI Agents
In src/sentry/monitors/logic/incidents.py around lines 36 to 59, guard against
previous_checkins being empty before accessing previous_checkins[0]; when the
sliced/reversed previous_checkins is empty (new monitor with immediate failure)
the any() check will be False but accessing index 0 will raise IndexError. Fix
by checking if previous_checkins is non-empty and using previous_checkins[0]
when present, otherwise set starting_checkin to the current failed_checkin (or
otherwise appropriate fallback), then continue updating monitor_env.status and
saving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants