Skip to content

Conversation

@evanpurkhiser
Copy link
Member

Add billing seat assignment and removal to UptimeDomainCheckFailureValidator
to ensure uptime detectors created, updated, or deleted via the detector APIs
correctly handle billing seats.

Changes:

  • Assign seats when creating enabled detectors (gracefully disable if no seats)
  • Assign/remove seats when enabling/disabling detectors via updates
  • Remove seats immediately when deleting detectors
  • Raise validation errors when enabling fails due to no available seats

Also add comprehensive tests to verify all billing seat operations work
correctly across create, update, and delete operations.

Fixes NEW-527: Ensure CRUD / enable+disable detectors in new UI handles assigning / unassigning seats

@evanpurkhiser evanpurkhiser requested a review from a team as a code owner November 7, 2025 21:03
@linear
Copy link

linear bot commented Nov 7, 2025

@evanpurkhiser evanpurkhiser requested review from a team and brendanhsentry November 7, 2025 21:03
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 7, 2025
Comment on lines 472 to 482
if detector.enabled:
try:
enable_uptime_detector(detector, ensure_assignment=True)
except UptimeMonitorNoSeatAvailable:
# No need to do anything if we failed to handle seat
# assignment. The monitor will be created, but not enabled
pass
else:
disable_uptime_detector(detector)

return detector
Copy link

Choose a reason for hiding this comment

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

Bug: Detector creation ignores enabled=False in validated_data, always defaulting to True due to db_default.
Severity: CRITICAL | Confidence: 0.95

🔍 Detailed Analysis

When creating a new detector via the API, the Detector model's enabled field defaults to True due to db_default=True. The BaseDetectorTypeValidator.create() method does not explicitly pass the enabled value from validated_data to the Detector constructor. Consequently, if a user requests {"enabled": False, ...}, the UptimeDomainCheckFailureValidator.create() method will incorrectly call enable_uptime_detector() because detector.enabled will always be True after creation, ignoring the user's intent.

💡 Suggested Fix

Ensure the enabled field from validated_data is explicitly passed to the Detector constructor in BaseDetectorTypeValidator.create() to override the database default when a value is provided.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/uptime/endpoints/validators.py#L469-L482

Potential issue: When creating a new detector via the API, the `Detector` model's
`enabled` field defaults to `True` due to `db_default=True`. The
`BaseDetectorTypeValidator.create()` method does not explicitly pass the `enabled` value
from `validated_data` to the `Detector` constructor. Consequently, if a user requests
`{"enabled": False, ...}`, the `UptimeDomainCheckFailureValidator.create()` method will
incorrectly call `enable_uptime_detector()` because `detector.enabled` will always be
`True` after creation, ignoring the user's intent.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines 488 to 510

if was_enabled != enabled:
if enabled:
try:
enable_uptime_detector(instance)
except UptimeMonitorNoSeatAvailable as err:
raise serializers.ValidationError({"enabled": [err.result.reason]})
else:
disable_uptime_detector(instance)

super().update(instance, validated_data)
Copy link

Choose a reason for hiding this comment

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

Bug: Detector enabled state is toggled twice during update, once by child and once by parent class.
Severity: HIGH | Confidence: 0.95

🔍 Detailed Analysis

In UptimeDomainCheckFailureValidator.update(), the detector's enabled state is toggled twice. First, the child class explicitly calls enable_uptime_detector() or disable_uptime_detector() based on the enabled state. Subsequently, super().update(instance, validated_data) is called. The parent class's update() method then re-processes the enabled field from validated_data and calls toggle_detector(), leading to redundant state changes. This can result in inconsistent states if the two toggle mechanisms have different behaviors.

💡 Suggested Fix

Remove the enabled field from validated_data before calling super().update() in UptimeDomainCheckFailureValidator.update() to prevent the parent class from re-processing it.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/uptime/endpoints/validators.py#L485-L498

Potential issue: In `UptimeDomainCheckFailureValidator.update()`, the detector's
`enabled` state is toggled twice. First, the child class explicitly calls
`enable_uptime_detector()` or `disable_uptime_detector()` based on the `enabled` state.
Subsequently, `super().update(instance, validated_data)` is called. The parent class's
`update()` method then re-processes the `enabled` field from `validated_data` and calls
`toggle_detector()`, leading to redundant state changes. This can result in inconsistent
states if the two toggle mechanisms have different behaviors.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah this is true, but it's not really a problem and I think it keeps this logic easier to maintain.

Copy link
Contributor

@saponifi3d saponifi3d left a comment

Choose a reason for hiding this comment

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

lgtm from the workflow engine perspective, might want another review from someone with more context over the other changes.

uptime_subscription = get_uptime_subscription(self.instance)
delete_uptime_subscription(uptime_subscription)

super().delete()
Copy link
Contributor

Choose a reason for hiding this comment

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

👀 yeah, this does feel a bit cleaner than having the hooks -- yay it's just one surface area now! 🎉

@evanpurkhiser evanpurkhiser force-pushed the evanpurkhiser/feat-uptime-add-billing-seat-management-for-detector-validators branch from b333159 to 674fb06 Compare November 7, 2025 21:21
# assignment. The monitor will be created, but not enabled
pass

return detector
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Disabled Features Still Activate

The create method unconditionally calls enable_uptime_detector regardless of the enabled value in validated_data. If a detector is created with enabled=False, it will still attempt to enable it and assign a seat, contradicting the user's intent and potentially consuming quota unnecessarily.

Fix in Cursor Fix in Web

Add billing seat assignment and removal to UptimeDomainCheckFailureValidator
to ensure uptime detectors created, updated, or deleted via the detector APIs
correctly handle billing seats.

Changes:
- Assign seats when creating enabled detectors (gracefully disable if no seats)
- Assign/remove seats when enabling/disabling detectors via updates
- Remove seats immediately when deleting detectors
- Raise validation errors when enabling fails due to no available seats

Also add comprehensive tests to verify all billing seat operations work
correctly across create, update, and delete operations.

Fixes [NEW-527: Ensure CRUD / enable+disable detectors in new UI handles assigning / unassigning seats](https://linear.app/getsentry/issue/NEW-527/ensure-crud-enabledisable-detectors-in-new-ui-handles-assigning)
@evanpurkhiser evanpurkhiser force-pushed the evanpurkhiser/feat-uptime-add-billing-seat-management-for-detector-validators branch from 674fb06 to 405c673 Compare November 7, 2025 21:26
enable_uptime_detector(instance)
else:
disable_uptime_detector(instance)

Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Conflicting Updates Lead to Detector Instability

The update method calls enable_uptime_detector or disable_uptime_detector which update detector.enabled, then immediately calls super().update(validated_data) which also updates detector.enabled via toggle_detector. This double update creates a race condition and potential state inconsistency because toggle_detector also modifies detector.status, which may conflict with the status management in the uptime-specific enable/disable functions. The enabled field should be removed from validated_data before calling super().update() to prevent the base class from re-processing it.

Fix in Cursor Fix in Web

detector = super().create(validated_data)

try:
enable_uptime_detector(detector, ensure_assignment=True)
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to worry about mode != UptimeMonitorMode.AUTO_DETECTED_ONBOARDING and creating disabled Detectors? or are those not a possible for this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah not applicable here. These will always be created in manual mode.

Copy link
Member

@brendanhsentry brendanhsentry left a comment

Choose a reason for hiding this comment

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

nice looks good to me

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

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants