Skip to content

Commit e39dae3

Browse files
Merge branch 'master' into mz/add-error-context-llm
2 parents cca6b97 + fe08250 commit e39dae3

File tree

9 files changed

+245
-14
lines changed

9 files changed

+245
-14
lines changed

devservices/config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ x-sentry-service-config:
132132
symbolicator: [postgres, snuba, symbolicator, spotlight]
133133
memcached: [postgres, snuba, memcached, spotlight]
134134
profiling: [postgres, snuba, vroom, spotlight]
135+
ingest:
136+
[
137+
snuba,
138+
postgres,
139+
relay,
140+
spotlight,
141+
worker,
142+
ingest-events,
143+
ingest-transactions,
144+
ingest-attachments,
145+
post-process-forwarder-errors,
146+
post-process-forwarder-transactions,
147+
post-process-forwarder-issue-platform,
148+
]
135149
minimal: [postgres, snuba]
136150
ingest-all:
137151
[
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
{
2+
"event_id": "5d6401994d7949d2ac3474f472564370",
3+
"platform": "node",
4+
"message": "",
5+
"datetime": "2025-05-12T22:42:38.642986+00:00",
6+
"breakdowns": {
7+
"span_ops": {
8+
"ops.db": {
9+
"value": 65.715075,
10+
"unit": "millisecond"
11+
},
12+
"total.time": {
13+
"value": 67.105293,
14+
"unit": "millisecond"
15+
}
16+
}
17+
},
18+
"request": {
19+
"url": "http://localhost:3001/vulnerable-login",
20+
"method": "GET",
21+
"query_string": [["type", "account"]]
22+
},
23+
"spans": [
24+
{
25+
"timestamp": 1747089758.567536,
26+
"start_timestamp": 1747089758.567,
27+
"exclusive_time": 0.536203,
28+
"op": "middleware.express",
29+
"span_id": "4a06692f4abc8dbe",
30+
"parent_span_id": "91fa92ff0205967d",
31+
"trace_id": "375a86eca09a4a4e91903838dd771f50",
32+
"status": "ok",
33+
"description": "corsMiddleware",
34+
"origin": "auto.http.otel.express",
35+
"data": {
36+
"express.name": "corsMiddleware",
37+
"express.type": "middleware",
38+
"sentry.op": "middleware.express",
39+
"sentry.origin": "auto.http.otel.express"
40+
},
41+
"sentry_tags": {
42+
"user": "ip:::1",
43+
"user.ip": "::1",
44+
"environment": "production",
45+
"transaction": "GET /vulnerable-login",
46+
"transaction.method": "GET",
47+
"transaction.op": "http.server",
48+
"browser.name": "Chrome",
49+
"sdk.name": "sentry.javascript.node",
50+
"sdk.version": "9.17.0",
51+
"platform": "node",
52+
"os.name": "macOS",
53+
"category": "middleware",
54+
"op": "middleware.express",
55+
"status": "ok",
56+
"trace.status": "ok"
57+
},
58+
"hash": "e6088cf8b370ed60"
59+
},
60+
{
61+
"timestamp": 1747089758.568761,
62+
"start_timestamp": 1747089758.568,
63+
"exclusive_time": 0.761032,
64+
"op": "middleware.express",
65+
"span_id": "92553d2584d250b8",
66+
"parent_span_id": "91fa92ff0205967d",
67+
"trace_id": "375a86eca09a4a4e91903838dd771f50",
68+
"status": "ok",
69+
"description": "jsonParser",
70+
"origin": "auto.http.otel.express",
71+
"data": {
72+
"express.name": "jsonParser",
73+
"express.type": "middleware",
74+
"sentry.op": "middleware.express",
75+
"sentry.origin": "auto.http.otel.express"
76+
},
77+
"sentry_tags": {
78+
"user": "ip:::1",
79+
"user.ip": "::1",
80+
"environment": "production",
81+
"transaction": "GET /vulnerable-login",
82+
"transaction.method": "GET",
83+
"transaction.op": "http.server",
84+
"browser.name": "Chrome",
85+
"sdk.name": "sentry.javascript.node",
86+
"sdk.version": "9.17.0",
87+
"platform": "node",
88+
"os.name": "macOS",
89+
"category": "middleware",
90+
"op": "middleware.express",
91+
"status": "ok",
92+
"trace.status": "ok"
93+
},
94+
"hash": "c81e963dad9ebc6c"
95+
},
96+
{
97+
"timestamp": 1747089758.569093,
98+
"start_timestamp": 1747089758.569,
99+
"exclusive_time": 0.092983,
100+
"op": "request_handler.express",
101+
"span_id": "435146ab0909419d",
102+
"parent_span_id": "91fa92ff0205967d",
103+
"trace_id": "375a86eca09a4a4e91903838dd771f50",
104+
"status": "ok",
105+
"description": "/vulnerable-login",
106+
"origin": "auto.http.otel.express",
107+
"data": {
108+
"express.name": "/vulnerable-login",
109+
"express.type": "request_handler",
110+
"http.route": "/vulnerable-login",
111+
"sentry.op": "request_handler.express",
112+
"sentry.origin": "auto.http.otel.express"
113+
},
114+
"sentry_tags": {
115+
"user": "ip:::1",
116+
"user.ip": "::1",
117+
"environment": "production",
118+
"transaction": "GET /vulnerable-login",
119+
"transaction.method": "GET",
120+
"transaction.op": "http.server",
121+
"browser.name": "Chrome",
122+
"sdk.name": "sentry.javascript.node",
123+
"sdk.version": "9.17.0",
124+
"platform": "node",
125+
"os.name": "macOS",
126+
"op": "request_handler.express",
127+
"status": "ok",
128+
"trace.status": "ok"
129+
},
130+
"hash": "872b0c84a6f1c590"
131+
},
132+
{
133+
"timestamp": 1747089758.637715,
134+
"start_timestamp": 1747089758.572,
135+
"exclusive_time": 65.715075,
136+
"op": "db",
137+
"span_id": "4703181ac343f71a",
138+
"parent_span_id": "91fa92ff0205967d",
139+
"trace_id": "375a86eca09a4a4e91903838dd771f50",
140+
"status": "ok",
141+
"description": "SELECT account.id, \"account\".name, account_type FROM data WHERE type = ?",
142+
"origin": "auto.db.otel.mysql2",
143+
"data": {
144+
"db.system": "mysql",
145+
"db.connection_string": "jdbc:mysql://localhost:3306/injection_test",
146+
"db.name": "injection_test",
147+
"db.statement": "SELECT account.id, \"account\".name, account_type FROM data WHERE type = ?",
148+
"db.user": "root",
149+
"net.peer.name": "localhost",
150+
"net.peer.port": 3306,
151+
"otel.kind": "CLIENT",
152+
"sentry.op": "db",
153+
"sentry.origin": "auto.db.otel.mysql2"
154+
},
155+
"hash": "45330ba0cafa5997"
156+
}
157+
]
158+
}

src/sentry/api/endpoints/organization_feedback_summary.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def get(self, request: Request, organization: Organization) -> Response:
7777

7878
if groups.count() < MIN_FEEDBACKS_TO_SUMMARIZE:
7979
logger.error("Too few feedbacks to summarize")
80-
return Response({"summary": None, "success": False, "num_feedbacks_used": 0})
80+
return Response({"summary": None, "success": False, "numFeedbacksUsed": 0})
8181

8282
# Also cap the number of characters that we send to the LLM
8383
group_feedbacks = []
@@ -100,5 +100,5 @@ def get(self, request: Request, organization: Organization) -> Response:
100100
return Response({"detail": "Error generating summary"}, status=500)
101101

102102
return Response(
103-
{"summary": summary, "success": True, "num_feedbacks_used": len(group_feedbacks)}
103+
{"summary": summary, "success": True, "numFeedbacksUsed": len(group_feedbacks)}
104104
)

src/sentry/eventstream/kafka/dispatch.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,5 @@ class EventPostProcessForwarderStrategyFactory(PostProcessForwarderStrategyFacto
100100
def _dispatch_function(
101101
message: Message[KafkaPayload], eventstream_type: str | None = None
102102
) -> None:
103-
return _get_task_kwargs_and_dispatch(message, eventstream_type)
103+
with _sampled_eventstream_timer(instance="_get_task_kwargs_and_dispatch"):
104+
return _get_task_kwargs_and_dispatch(message, eventstream_type)

src/sentry/performance_issues/detectors/sql_injection_detector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ def visit_span(self, span: Span) -> None:
122122
for parameter in self.request_parameters:
123123
value = parameter[1]
124124
key = parameter[0]
125-
if re.search(f"\\b{re.escape(key)}\\b", description) and re.search(
126-
f"\\b{re.escape(value)}\\b", description
125+
if re.search(rf'(?<![\w.])"?{re.escape(key)}"?(?![\w."])', description) and re.search(
126+
rf'(?<![\w.])"?{re.escape(value)}"?(?![\w."])', description
127127
):
128128
description = description.replace(value, "?")
129129
vulnerable_parameters.append(key)

src/sentry/workflow_engine/processors/delayed_workflow.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from collections import defaultdict
3+
from collections.abc import Mapping, Sequence
34
from dataclasses import dataclass
45
from datetime import timedelta
56
from typing import Any
@@ -35,6 +36,7 @@
3536
from sentry.utils.retries import ConditionalRetryPolicy, exponential_delay
3637
from sentry.workflow_engine.handlers.condition.event_frequency_query_handlers import (
3738
BaseEventFrequencyQueryHandler,
39+
QueryFilter,
3840
QueryResult,
3941
slow_condition_query_handler_registry,
4042
)
@@ -83,7 +85,26 @@ class UniqueConditionQuery:
8385
interval: str
8486
environment_id: int | None
8587
comparison_interval: str | None = None
86-
filters: list[dict[str, Any]] | None = None
88+
# Hashable representation of the filters
89+
frozen_filters: Sequence[frozenset[tuple[str, Any]]] | None = None
90+
91+
@staticmethod
92+
def freeze_filters(
93+
filters: Sequence[Mapping[str, Any]] | None,
94+
) -> Sequence[frozenset[tuple[str, Any]]] | None:
95+
"""
96+
Convert the sorted representation of filters into a frozen one that can
97+
be safely hashed.
98+
"""
99+
if filters is None:
100+
return None
101+
return tuple(frozenset(sorted(filter.items())) for filter in filters)
102+
103+
@property
104+
def filters(self) -> list[QueryFilter] | None:
105+
if self.frozen_filters is None:
106+
return None
107+
return [dict(filter) for filter in self.frozen_filters]
87108

88109
def __repr__(self):
89110
return f"UniqueConditionQuery(handler={self.handler.__name__}, interval={self.interval}, environment_id={self.environment_id}, comparison_interval={self.comparison_interval}, filters={self.filters})"
@@ -203,7 +224,7 @@ def generate_unique_queries(
203224
handler=handler,
204225
interval=condition.comparison["interval"],
205226
environment_id=environment_id,
206-
filters=condition.comparison.get("filters"),
227+
frozen_filters=UniqueConditionQuery.freeze_filters(condition.comparison.get("filters")),
207228
)
208229
]
209230
if condition_type in PERCENT_CONDITIONS:
@@ -213,7 +234,9 @@ def generate_unique_queries(
213234
interval=condition.comparison["interval"],
214235
environment_id=environment_id,
215236
comparison_interval=condition.comparison.get("comparison_interval"),
216-
filters=condition.comparison.get("filters"),
237+
frozen_filters=UniqueConditionQuery.freeze_filters(
238+
condition.comparison.get("filters")
239+
),
217240
)
218241
)
219242
return unique_queries

tests/sentry/api/endpoints/test_organization_feedback_summary.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def test_get_feedback_summary_basic(self, mock_generate_summary):
9191

9292
assert response.data["success"] is True
9393
assert response.data["summary"] == "Test summary of feedback"
94-
assert response.data["num_feedbacks_used"] == 15
94+
assert response.data["numFeedbacksUsed"] == 15
9595

9696
@django_db_all
9797
@patch(
@@ -122,7 +122,7 @@ def test_get_feedback_summary_with_date_filter(self, mock_generate_summary):
122122

123123
assert response.data["success"] is True
124124
assert response.data["summary"] == "Test summary of feedback"
125-
assert response.data["num_feedbacks_used"] == 12
125+
assert response.data["numFeedbacksUsed"] == 12
126126

127127
@django_db_all
128128
@patch(
@@ -151,7 +151,7 @@ def test_get_feedback_summary_with_project_filter(self, mock_generate_summary):
151151

152152
assert response.data["success"] is True
153153
assert response.data["summary"] == "Test summary of feedback"
154-
assert response.data["num_feedbacks_used"] == 10
154+
assert response.data["numFeedbacksUsed"] == 10
155155

156156
@django_db_all
157157
@patch(
@@ -180,7 +180,7 @@ def test_get_feedback_summary_with_many_project_filter_as_list(self, mock_genera
180180

181181
assert response.data["success"] is True
182182
assert response.data["summary"] == "Test summary of feedback"
183-
assert response.data["num_feedbacks_used"] == 22
183+
assert response.data["numFeedbacksUsed"] == 22
184184

185185
@django_db_all
186186
@patch(
@@ -208,7 +208,7 @@ def test_get_feedback_summary_with_many_project_filter_separate(self, mock_gener
208208
assert response.status_code == 200
209209
assert response.data["success"] is True
210210
assert response.data["summary"] == "Test summary of feedback"
211-
assert response.data["num_feedbacks_used"] == 22
211+
assert response.data["numFeedbacksUsed"] == 22
212212

213213
@django_db_all
214214
@patch(
@@ -259,4 +259,4 @@ def test_get_feedback_summary_character_limit(self, mock_generate_summary):
259259

260260
assert response.data["success"] is True
261261
assert response.data["summary"] == "Test summary of feedback"
262-
assert response.data["num_feedbacks_used"] == 12
262+
assert response.data["numFeedbacksUsed"] == 12

tests/sentry/performance_issues/test_sql_injection_detector.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ def test_sql_injection_detection_in_body(self):
5454
assert problem.evidence_data["vulnerable_parameters"] == ["username"]
5555
assert problem.evidence_data["request_url"] == "http://localhost:3001/vulnerable-login"
5656

57+
def test_sql_injection_regex(self):
58+
injection_event = get_event("sql-injection/sql-injection-test-regex-event")
59+
assert len(self.find_problems(injection_event)) == 0
60+
5761
def test_sql_injection_on_non_vulnerable_query(self):
5862
injection_event = get_event("sql-injection/sql-injection-event-non-vulnerable")
5963
assert len(self.find_problems(injection_event)) == 0

tests/sentry/workflow_engine/processors/test_delayed_workflow.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sentry.models.project import Project
1414
from sentry.notifications.models.notificationaction import ActionTarget
1515
from sentry.rules.conditions.event_frequency import ComparisonType
16+
from sentry.rules.match import MatchType
1617
from sentry.rules.processing.buffer_processing import process_in_batches
1718
from sentry.rules.processing.delayed_processing import fetch_project
1819
from sentry.testutils.helpers import override_options, with_feature
@@ -353,6 +354,36 @@ def test_generate_unique_queries(self):
353354
expected_comparison_query = UniqueConditionQuery(**comparison_query_dict)
354355
assert percent_queries[1] == expected_comparison_query
355356

357+
def test_generate_unique_queries__filters_hashable(self):
358+
dc = self.create_data_condition(
359+
condition_group=self.create_data_condition_group(
360+
logic_type=DataConditionGroup.Type.ALL
361+
),
362+
type=Condition.EVENT_FREQUENCY_COUNT,
363+
comparison={
364+
"interval": "1h",
365+
"value": 100,
366+
"filters": [
367+
{
368+
"key": "http.method",
369+
"match": MatchType.IS_IN,
370+
"value": "GET,POST",
371+
}
372+
],
373+
},
374+
condition_result=True,
375+
)
376+
queries = generate_unique_queries(dc, None)
377+
[hash(query) for query in queries] # shouldn't raise
378+
assert len(queries) == 1
379+
assert queries[0].filters == [
380+
{
381+
"key": "http.method",
382+
"match": MatchType.IS_IN,
383+
"value": "GET,POST",
384+
}
385+
]
386+
356387
def test_generate_unique_queries__invalid(self):
357388
dc = self.create_data_condition(
358389
condition_group=self.workflow_triggers,

0 commit comments

Comments
 (0)