Skip to content

Commit dc6c46b

Browse files
fix(explorer): Properly escape transaction names in Snuba queries (#97591)
Fixes [SEER-2X5](https://sentry.io/organizations/sentry/issues/6803858981/). The issue was that: Transaction name containing SQL query unescaped into Snuba search query, causing parse error and 400 Bad Request. - Encloses the transaction name in double quotes when constructing the Snuba query string in `index_data.py`. This prevents parsing errors when the transaction name contains special characters, such as SQL queries. - Adds a test script `test_transaction_name_escaping.py` to verify that transaction names with problematic characters are properly escaped. This fix was generated by Seer in Sentry, triggered by Rohan Agarwal. 👁️ Run ID: 752838 Not quite right? [Click here to continue debugging with Seer.](https://sentry.io/organizations/sentry/issues/6803858981/?seerDrawer=true) ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --------- Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com> Co-authored-by: Rohan Agarwal <[email protected]>
1 parent 028e531 commit dc6c46b

File tree

2 files changed

+77
-2
lines changed

2 files changed

+77
-2
lines changed

src/sentry/seer/explorer/index_data.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
from datetime import UTC, datetime, timedelta
34
from typing import Any
45

@@ -33,6 +34,9 @@
3334

3435
logger = logging.getLogger(__name__)
3536

37+
# Regex to match unescaped quotes (not preceded by backslash)
38+
UNESCAPED_QUOTE_RE = re.compile('(?<!\\\\)"')
39+
3640

3741
def get_transactions_for_project(project_id: int) -> list[Transaction]:
3842
"""
@@ -137,9 +141,10 @@ def get_trace_for_transaction(transaction_name: str, project_id: int) -> TraceDa
137141
)
138142

139143
# Step 1: Get trace IDs with their span counts in a single query
144+
escaped_transaction_name = UNESCAPED_QUOTE_RE.sub('\\"', transaction_name)
140145
traces_result = Spans.run_table_query(
141146
params=snuba_params,
142-
query_string=f"transaction:{transaction_name} project.id:{project_id}",
147+
query_string=f'transaction:"{escaped_transaction_name}" project.id:{project_id}',
143148
selected_columns=[
144149
"trace",
145150
"count()", # This counts all spans in each trace
@@ -462,7 +467,8 @@ def get_issues_for_transaction(transaction_name: str, project_id: int) -> Transa
462467
start_time = end_time - timedelta(hours=24)
463468

464469
# Step 1: Search for issues using transaction filter
465-
query = f'is:unresolved transaction:"{transaction_name}"'
470+
escaped_transaction_name = UNESCAPED_QUOTE_RE.sub('\\"', transaction_name)
471+
query = f'is:unresolved transaction:"{escaped_transaction_name}"'
466472
search_filters = parse_and_convert_issue_search_query(
467473
query, project.organization, [project], [], AnonymousUser()
468474
)

tests/sentry/seer/explorer/test_index_data.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,3 +706,72 @@ def test_get_issues_for_transaction(self) -> None:
706706
assert (
707707
"tags" in issue.events[0] or issue.events[0].get("transaction") == transaction_name
708708
)
709+
710+
def test_get_issues_for_transaction_with_quotes(self) -> None:
711+
"""Test that transaction names with quotes and search operators are properly escaped in search queries."""
712+
# Test case 1: Transaction name with quotes that would break search syntax if not escaped
713+
transaction_name_quotes = 'GET /api/users/"john"/profile'
714+
715+
# Create an event/issue for the transaction with quotes
716+
event1 = self.store_event(
717+
data={
718+
"message": "Authentication failed for quoted user",
719+
"tags": [["transaction", transaction_name_quotes]],
720+
"fingerprint": ["auth-error-quotes"],
721+
"platform": "python",
722+
"timestamp": self.ten_mins_ago.isoformat(),
723+
"level": "error",
724+
},
725+
project_id=self.project.id,
726+
)
727+
728+
# Test case 2: Transaction name with " IN " operator that could break search syntax
729+
transaction_name_in = "POST /api/check IN database/users"
730+
731+
# Create an event/issue for the transaction with IN operator
732+
event2 = self.store_event(
733+
data={
734+
"message": "Database operation failed",
735+
"tags": [["transaction", transaction_name_in]],
736+
"fingerprint": ["database-in-error"],
737+
"platform": "python",
738+
"timestamp": self.ten_mins_ago.isoformat(),
739+
"level": "error",
740+
},
741+
project_id=self.project.id,
742+
)
743+
744+
# Verify both events were stored with correct transaction names
745+
latest_event1 = event1.group.get_latest_event()
746+
transaction_tag1 = latest_event1.get_tag("transaction")
747+
assert transaction_tag1 == transaction_name_quotes
748+
749+
latest_event2 = event2.group.get_latest_event()
750+
transaction_tag2 = latest_event2.get_tag("transaction")
751+
assert transaction_tag2 == transaction_name_in
752+
753+
# Test quotes case - this should not crash despite quotes in transaction name
754+
result1 = get_issues_for_transaction(transaction_name_quotes, self.project.id)
755+
756+
# Verify the quotes result
757+
assert result1 is not None
758+
assert result1.transaction_name == transaction_name_quotes
759+
assert result1.project_id == self.project.id
760+
assert len(result1.issues) == 1
761+
762+
issue1 = result1.issues[0]
763+
assert issue1.id == event1.group.id
764+
assert issue1.transaction == transaction_name_quotes
765+
766+
# Test IN operator case - this should not crash despite IN operator in transaction name
767+
result2 = get_issues_for_transaction(transaction_name_in, self.project.id)
768+
769+
# Verify the IN operator result
770+
assert result2 is not None
771+
assert result2.transaction_name == transaction_name_in
772+
assert result2.project_id == self.project.id
773+
assert len(result2.issues) == 1
774+
775+
issue2 = result2.issues[0]
776+
assert issue2.id == event2.group.id
777+
assert issue2.transaction == transaction_name_in

0 commit comments

Comments
 (0)