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
47 changes: 30 additions & 17 deletions src/sentry/incidents/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from copy import deepcopy
from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone
from re import Match
from typing import Any, TypedDict
from uuid import UUID, uuid4

Expand All @@ -24,6 +25,7 @@
from sentry.db.models import Model
from sentry.db.models.manager.base_query_set import BaseQuerySet
from sentry.deletions.models.scheduleddeletion import CellScheduledDeletion
from sentry.discover.arithmetic import is_equation, parse_arithmetic, strip_equation

Check warning on line 28 in src/sentry/incidents/logic.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

ArithmeticError exceptions from parse_arithmetic not caught in alert validation

The newly imported `parse_arithmetic` function (line 28) can raise `ArithmeticParseError`, `ArithmeticValidationError`, or `MaxOperatorError` when parsing malformed equations. At line 1909, `parse_arithmetic(strip_equation(aggregate))` is called without handling these exceptions. The caller in `snuba_query_validator.py` only catches `InvalidSearchQuery`, so arithmetic parse failures will propagate as 500 internal server errors instead of 400 validation errors. A user submitting `equation|5 + + 5` or `equation|5 / 0` would trigger this.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ArithmeticError exceptions from parse_arithmetic not caught in alert validation

The newly imported parse_arithmetic function (line 28) can raise ArithmeticParseError, ArithmeticValidationError, or MaxOperatorError when parsing malformed equations. At line 1909, parse_arithmetic(strip_equation(aggregate)) is called without handling these exceptions. The caller in snuba_query_validator.py only catches InvalidSearchQuery, so arithmetic parse failures will propagate as 500 internal server errors instead of 400 validation errors. A user submitting equation|5 + + 5 or equation|5 / 0 would trigger this.

Verification

Read src/sentry/discover/arithmetic.py to confirm ArithmeticError hierarchy (lines 20-33). Read src/sentry/incidents/logic.py to find usage at line 1909. Read src/sentry/snuba/snuba_query_validator.py lines 285-295 to confirm only InvalidSearchQuery is caught. ArithmeticError is not a subclass of InvalidSearchQuery.

Also found at 2 additional locations
  • src/sentry/search/eap/trace_metrics/validator.py:6-6
  • src/sentry/search/eap/trace_metrics/validator.py:63-63

Identified by Warden sentry-backend-bugs · 7ZJ-VR5

from sentry.incidents import tasks
from sentry.incidents.events import IncidentCreatedEvent, IncidentStatusUpdatedEvent
from sentry.incidents.models.alert_rule import (
Expand Down Expand Up @@ -1848,12 +1850,15 @@


def get_column_from_aggregate(
aggregate: str, allow_mri: bool, allow_eap: bool = False
aggregate: str,
allow_mri: bool,
allow_eap: bool = False,
match: Match[str] | None = None,
) -> str | None:
# These functions exist as SnQLFunction definitions and are not supported in the older
# logic for resolving functions. We parse these using `fields.is_function`, otherwise
# they will fail using the old resolve_field logic.
match = is_function(aggregate)
match = is_function(aggregate) if match is None else match
if match and (
match.group("function") in SPANS_METRICS_FUNCTIONS
or match.group("function") in METRICS_LAYER_UNSUPPORTED_TRANSACTION_METRICS_FUNCTIONS
Expand Down Expand Up @@ -1900,21 +1905,29 @@
aggregate: str, allow_mri: bool = False, allow_eap: bool = False
) -> bool:
# TODO(ddm): remove `allow_mri` once the experimental feature flag is removed.
column = get_column_from_aggregate(aggregate, allow_mri, allow_eap)
match = is_function(aggregate)
function = match.group("function") if match else None
return (
column is None
or is_measurement(column)
or column in SUPPORTED_COLUMNS
or column in TRANSLATABLE_COLUMNS
or (is_mri(column) and allow_mri)
or (
isinstance(function, str)
and column in INSIGHTS_FUNCTION_VALID_ARGS_MAP.get(function, [])
)
or allow_eap
)
if is_equation(aggregate):
_, _, terms = parse_arithmetic(strip_equation(aggregate))

Check warning on line 1909 in src/sentry/incidents/logic.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

Equation validation skips field validation by only checking functions

The `parse_arithmetic` function returns `(result, fields, functions)` but the code only unpacks the third element (functions) into `terms`, completely ignoring the second element (fields). This means equations containing raw fields (e.g., `equation|transaction.duration / 1000`) will have an empty `terms` list and return True without validation. Similarly, mixed equations like `equation|count(project) + transaction.duration` will only validate the function, not the field.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Equation validation skips field validation by only checking functions

The parse_arithmetic function returns (result, fields, functions) but the code only unpacks the third element (functions) into terms, completely ignoring the second element (fields). This means equations containing raw fields (e.g., equation|transaction.duration / 1000) will have an empty terms list and return True without validation. Similarly, mixed equations like equation|count(project) + transaction.duration will only validate the function, not the field.

Verification

Read src/sentry/discover/arithmetic.py to confirm parse_arithmetic returns tuple[Operation|float|str, list[str], list[str]] where second element is fields and third is functions (lines 338-363). Verified that ArithmeticVisitor tracks fields and functions separately (visit_field_value adds to self.fields, visit_function_value adds to self.functions). Confirmed field_allowlist includes raw fields like 'transaction.duration' that can be used directly in equations.

Suggested fix: Unpack both fields and functions from parse_arithmetic and iterate over both lists, or combine them into a single terms list.

Suggested change
_, _, terms = parse_arithmetic(strip_equation(aggregate))
_, fields, functions = parse_arithmetic(strip_equation(aggregate))
terms = fields + functions

Identified by Warden sentry-backend-bugs · KKV-XLH

Comment on lines +1908 to +1909
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: parse_arithmetic can raise ArithmeticParseError/ArithmeticValidationError which are not InvalidSearchQuery, causing uncaught exceptions in callers.
Severity: MEDIUM

Suggested Fix

Wrap the parse_arithmetic call in a try/except that catches ArithmeticError (or its subclasses) and either returns False or raises InvalidSearchQuery so the caller's existing error handling can process it. For example: try: _, _, terms = parse_arithmetic(strip_equation(aggregate)) except ArithmeticError: return False.

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/incidents/logic.py#L1908-L1909

Potential issue: When `check_aggregate_column_support` receives a malformed equation
(e.g., `equation|+++`), `parse_arithmetic` raises `ArithmeticParseError` (a subclass of
`ArithmeticError`). The caller in `snuba_query_validator.py` (`_validate_aggregate`)
only catches `InvalidSearchQuery`, not `ArithmeticError`. Since `ArithmeticError` and
`InvalidSearchQuery` are unrelated exception hierarchies, the error propagates uncaught,
resulting in a 500 server error instead of a proper validation error response. A user
submitting a malformed equation string in an alert rule creation/update request would
trigger this crash.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unhandled ArithmeticError causes 500 on malformed equations

High Severity

parse_arithmetic can raise ArithmeticParseError, ArithmeticValidationError, or MaxOperatorError (all inheriting from ArithmeticError(Exception)). Neither check_aggregate_column_support nor validate_trace_metrics_aggregate catches these. The caller in snuba_query_validator.py only catches InvalidSearchQuery, which is a completely separate exception hierarchy. A malformed equation string would result in an unhandled 500 error instead of a proper validation error.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7050ec. Configure here.

else:
terms = [aggregate]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Equation validation skips non-function terms entirely

Medium Severity

parse_arithmetic returns (result, fields, functions). The unpacking _, _, terms only captures the functions list (third element), discarding the fields list. Equations containing only field-based terms or purely numeric expressions (e.g., equation|5 * 2) produce an empty terms list, causing the validation loop to be skipped entirely and allowing invalid aggregates to pass.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7050ec. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Equation strings crash in translate_aggregate_field after passing validation

High Severity

check_aggregate_column_support now accepts equation strings in the non-trace-metrics path, but the immediately subsequent call to translate_aggregate_field in the validator was not updated to handle them. translate_aggregate_field passes the raw equation string to get_column_from_aggregate, which calls resolve_field on it — that raises InvalidSearchQuery because the equation string contains invalid field characters. This exception is thrown outside the try/except InvalidSearchQuery block in _validate_aggregate, resulting in an unhandled crash.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7050ec. Configure here.


for term in terms:
match = is_function(term)
column = get_column_from_aggregate(term, allow_mri, allow_eap, match)
function = match.group("function") if match else None
if not (
column is None
or is_measurement(column)
or column in SUPPORTED_COLUMNS
or column in TRANSLATABLE_COLUMNS
or (is_mri(column) and allow_mri)
or (
isinstance(function, str)
and column in INSIGHTS_FUNCTION_VALID_ARGS_MAP.get(function, [])
)
or allow_eap
):
return False
return True


def translate_aggregate_field(
Expand Down
27 changes: 17 additions & 10 deletions src/sentry/search/eap/trace_metrics/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from rest_framework import serializers

from sentry.discover.arithmetic import is_equation, parse_arithmetic, strip_equation

Check warning on line 6 in src/sentry/search/eap/trace_metrics/validator.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

[7ZJ-VR5] ArithmeticError exceptions from parse_arithmetic not caught in alert validation (additional location)

The newly imported `parse_arithmetic` function (line 28) can raise `ArithmeticParseError`, `ArithmeticValidationError`, or `MaxOperatorError` when parsing malformed equations. At line 1909, `parse_arithmetic(strip_equation(aggregate))` is called without handling these exceptions. The caller in `snuba_query_validator.py` only catches `InvalidSearchQuery`, so arithmetic parse failures will propagate as 500 internal server errors instead of 400 validation errors. A user submitting `equation|5 + + 5` or `equation|5 / 0` would trigger this.
from sentry.exceptions import InvalidSearchQuery
from sentry.search.eap.resolver import SearchResolver
from sentry.search.eap.trace_metrics.config import TraceMetricsSearchResolverConfig
Expand Down Expand Up @@ -58,14 +59,20 @@
Raises:
serializers.ValidationError: If the aggregate is invalid
"""
try:
trace_metric = extract_trace_metric_from_aggregate(aggregate)
if trace_metric is None:
raise InvalidSearchQuery(
f"Trace metrics aggregate {aggregate} must specify metric name, type, and unit"
if is_equation(aggregate):
_, _, terms = parse_arithmetic(strip_equation(aggregate))

Check warning on line 63 in src/sentry/search/eap/trace_metrics/validator.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

[7ZJ-VR5] ArithmeticError exceptions from parse_arithmetic not caught in alert validation (additional location)

The newly imported `parse_arithmetic` function (line 28) can raise `ArithmeticParseError`, `ArithmeticValidationError`, or `MaxOperatorError` when parsing malformed equations. At line 1909, `parse_arithmetic(strip_equation(aggregate))` is called without handling these exceptions. The caller in `snuba_query_validator.py` only catches `InvalidSearchQuery`, so arithmetic parse failures will propagate as 500 internal server errors instead of 400 validation errors. A user submitting `equation|5 + + 5` or `equation|5 / 0` would trigger this.
Comment on lines +62 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: parse_arithmetic can raise ArithmeticError outside the try/except InvalidSearchQuery block, causing unhandled 500 errors.
Severity: MEDIUM

Suggested Fix

Wrap the parse_arithmetic call in a try/except that catches ArithmeticError and converts it to a serializers.ValidationError. For example: try: _, _, terms = parse_arithmetic(strip_equation(aggregate)) except ArithmeticError as e: raise serializers.ValidationError({"aggregate": f"Invalid equation: {e}"}).

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/search/eap/trace_metrics/validator.py#L62-L63

Potential issue: In `validate_trace_metrics_aggregate`, the `parse_arithmetic` call at
line 63 is outside the `try/except InvalidSearchQuery` block (which starts at line 68).
If a user submits a malformed equation string, `parse_arithmetic` raises
`ArithmeticParseError` or `ArithmeticValidationError`, both subclasses of
`ArithmeticError` (not `InvalidSearchQuery`). This exception propagates uncaught,
resulting in a 500 server error instead of a proper `serializers.ValidationError`
response. This is a realistic scenario when users input invalid arithmetic expressions
in trace metrics alert configurations.

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

else:
terms = [aggregate]

for term in terms:
try:
trace_metric = extract_trace_metric_from_aggregate(term)
if trace_metric is None:
raise InvalidSearchQuery(
f"Trace metrics aggregate {term} must specify metric name, type, and unit"
)
except InvalidSearchQuery as e:
logger.exception(f"Invalid trace metrics aggregate: {term} {e}")
raise serializers.ValidationError(
{"aggregate": f"Invalid trace metrics aggregate: {term}"}
)
except InvalidSearchQuery as e:
logger.exception("Invalid trace metrics aggregate: %s %s", aggregate, e)
raise serializers.ValidationError(
{"aggregate": f"Invalid trace metrics aggregate: {aggregate}"}
)
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,58 @@ def test_use_transactions_instead_of_generic_metrics_dataset(self) -> None:
assert query_sub.snuba_query.aggregate == "count()"
assert query_sub.snuba_query.event_types == [SnubaQueryEventType.EventType.TRANSACTION]

@with_feature(
[
"organizations:tracemetrics-alerts",
"organizations:tracemetrics-enabled",
]
)
def test_use_metrics_equation_as_aggregate(self) -> None:
data = {**self.valid_data}
equation = 'equation|count_if(`agent_name:"Agent Run"`,value,request_duration,distribution,none) * 2'
data["dataSources"] = [
{
"queryType": SnubaQuery.Type.PERFORMANCE.value,
"dataset": Dataset.EventsAnalyticsPlatform.value,
"query": "",
"aggregate": equation,
"timeWindow": 300,
"environment": self.environment.name,
"eventTypes": [SnubaQueryEventType.EventType.TRACE_ITEM_METRIC.name.lower()],
}
]

with self.tasks():
response = self.get_success_response(
self.organization.slug,
self.project.slug,
**data,
status_code=201,
)

assert (
response.data["dataSources"][0]["queryObj"]["snubaQuery"]["dataset"]
== Dataset.EventsAnalyticsPlatform.value
)
assert response.data["dataSources"][0]["queryObj"]["snubaQuery"]["query"] == ""
assert response.data["dataSources"][0]["queryObj"]["snubaQuery"]["aggregate"] == equation

detector = Detector.objects.get(id=response.data["id"])
data_source = DataSource.objects.get(detector=detector)
assert data_source.type == data_source_type_registry.get_key(
QuerySubscriptionDataSourceHandler
)
assert data_source.organization_id == self.organization.id
query_sub = QuerySubscription.objects.get(id=int(data_source.source_id))
assert query_sub.project == self.project
assert query_sub.snuba_query.type == SnubaQuery.Type.PERFORMANCE.value
assert query_sub.snuba_query.dataset == Dataset.EventsAnalyticsPlatform.value
assert query_sub.snuba_query.query == ""
assert query_sub.snuba_query.aggregate == equation
assert query_sub.snuba_query.event_types == [
SnubaQueryEventType.EventType.TRACE_ITEM_METRIC
]


@cell_silo_test
class OrganizationProjectDetectorIndexMonitorPostTest(APITestCase):
Expand Down
Loading