Skip to content

Commit 33a81d8

Browse files
authored
test(occurrences on eap): Create e2e test infrastructure (#108179)
Adds the `OccurrenceTestCase` test fixture with `create_eap_occurrence()` & `store_occurrences()` for writing occurrence trace items to EAP in tests, supporting both lightweight EAP-only tests and dual-write tests via setting the `eventstream.eap_forwarding_rate` option value. Using the former is preferred when dual-write functionality is not needed in tests, since this option & the Snuba write path will eventually be removed, and we will eventually be writing only to EAP. Extracts `build_occurrence_attributes()` in `item_helpers.py` as a shared encoding function used by the production `encode_attributes()` path and the test fixture. Adds end-to-end tests for the `is_escalating` EAP read path (`get_group_hourly_count`, `query_groups_past_counts`). Replaces the mock-heavy unit tests which are no redundant with the new e2e coverage. Follow-up items: - [ ] Migrate all other EAP read path tests to use the e2e test mixin - [ ] See if any logic common to all of the tests can be abstracted into the mixin
1 parent 4e42f2c commit 33a81d8

File tree

5 files changed

+391
-309
lines changed

5 files changed

+391
-309
lines changed

src/sentry/eventstream/item_helpers.py

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,46 @@ def serialize_event_data_as_item(
3434
Timestamp(seconds=int(event_data["received"])) if "received" in event_data else None
3535
),
3636
retention_days=event_data.get("retention_days", 90),
37-
attributes=encode_attributes(
37+
attributes=_encode_attributes(
3838
event, event_data, ignore_fields={"event_id", "timestamp", "tags", "spans", "'spans'"}
3939
),
4040
)
4141

4242

43+
def _encode_attributes(
44+
event: Event | GroupEvent, event_data: Mapping[str, Any], ignore_fields: set[str] | None = None
45+
) -> Mapping[str, AnyValue]:
46+
raw_tags = event_data.get("tags") or []
47+
tags_dict = {kv[0]: kv[1] for kv in raw_tags if kv is not None and kv[1] is not None}
48+
49+
all_ignore_fields = (ignore_fields or set()) | {"tags"}
50+
attributes = _build_occurrence_attributes(
51+
event_data, tags=tags_dict, ignore_fields=all_ignore_fields
52+
)
53+
54+
if event.group_id:
55+
attributes["group_id"] = AnyValue(int_value=event.group_id)
56+
57+
return attributes
58+
59+
60+
def _build_occurrence_attributes(
61+
data: Mapping[str, Any],
62+
tags: Mapping[str, str] | None = None,
63+
ignore_fields: set[str] | None = None,
64+
) -> dict[str, AnyValue]:
65+
ignore_fields = ignore_fields or set()
66+
attributes: dict[str, AnyValue] = {
67+
k: _encode_value(v) for k, v in data.items() if k not in ignore_fields and v is not None
68+
}
69+
70+
tag_attrs = {f"tags[{k}]": _encode_value(v) for k, v in (tags or {}).items()}
71+
attributes.update(tag_attrs)
72+
attributes["tag_keys"] = _encode_value(sorted(tag_attrs.keys()))
73+
74+
return attributes
75+
76+
4377
def _encode_value(value: Any, _depth: int = 0) -> AnyValue:
4478
if _depth > _ENCODE_MAX_DEPTH:
4579
# Beyond max depth, stringify to prevent protobuf nesting limit errors.
@@ -78,39 +112,3 @@ def _encode_value(value: Any, _depth: int = 0) -> AnyValue:
78112
)
79113
else:
80114
raise NotImplementedError(f"encode not supported for {type(value)}")
81-
82-
83-
def encode_attributes(
84-
event: Event | GroupEvent, event_data: Mapping[str, Any], ignore_fields: set[str] | None = None
85-
) -> Mapping[str, AnyValue]:
86-
attributes = {}
87-
ignore_fields = ignore_fields or set()
88-
89-
for key, value in event_data.items():
90-
if key in ignore_fields:
91-
continue
92-
if value is None:
93-
continue
94-
attributes[key] = _encode_value(value)
95-
96-
if event.group_id:
97-
attributes["group_id"] = AnyValue(int_value=event.group_id)
98-
99-
format_tag_key = lambda key: f"tags[{key}]"
100-
101-
tag_keys = set()
102-
tags = event_data.get("tags")
103-
if tags is not None:
104-
for tag in tags:
105-
if tag is None:
106-
continue
107-
key, value = tag
108-
if value is None:
109-
continue
110-
formatted_key = format_tag_key(key)
111-
attributes[formatted_key] = _encode_value(value)
112-
tag_keys.add(formatted_key)
113-
114-
attributes["tag_keys"] = _encode_value(sorted(tag_keys))
115-
116-
return attributes

src/sentry/testutils/cases.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
CheckStatus,
5151
CheckStatusReason,
5252
)
53-
from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType
53+
from sentry_protos.snuba.v1.request_common_pb2 import (
54+
TRACE_ITEM_TYPE_OCCURRENCE,
55+
TraceItemType,
56+
)
5457
from sentry_protos.snuba.v1.trace_item_pb2 import AnyValue, TraceItem
5558
from sentry_relay.consts import SPAN_STATUS_NAME_TO_CODE
5659
from slack_sdk.web import SlackResponse
@@ -62,7 +65,9 @@
6265
from sentry.auth.authenticators.totp import TotpInterface
6366
from sentry.auth.provider import Provider
6467
from sentry.auth.providers.dummy import DummyProvider
65-
from sentry.auth.providers.saml2.activedirectory.apps import ACTIVE_DIRECTORY_PROVIDER_NAME
68+
from sentry.auth.providers.saml2.activedirectory.apps import (
69+
ACTIVE_DIRECTORY_PROVIDER_NAME,
70+
)
6671
from sentry.auth.staff import COOKIE_DOMAIN as STAFF_COOKIE_DOMAIN
6772
from sentry.auth.staff import COOKIE_NAME as STAFF_COOKIE_NAME
6873
from sentry.auth.staff import COOKIE_PATH as STAFF_COOKIE_PATH
@@ -77,6 +82,7 @@
7782
from sentry.auth.superuser import SUPERUSER_ORG_ID, Superuser
7883
from sentry.conf.types.kafka_definition import Topic, get_topic_codec
7984
from sentry.event_manager import EventManager
85+
from sentry.eventstream.item_helpers import _build_occurrence_attributes
8086
from sentry.eventstream.snuba import SnubaEventStream
8187
from sentry.issue_detection.performance_detection import detect_performance_problems
8288
from sentry.issues.grouptype import (
@@ -108,8 +114,12 @@
108114
from sentry.models.rule import RuleSource
109115
from sentry.monitors.models import Monitor, MonitorEnvironment, ScheduleType
110116
from sentry.new_migrations.monkey.state import SentryProjectState
111-
from sentry.notifications.models.notificationsettingoption import NotificationSettingOption
112-
from sentry.notifications.models.notificationsettingprovider import NotificationSettingProvider
117+
from sentry.notifications.models.notificationsettingoption import (
118+
NotificationSettingOption,
119+
)
120+
from sentry.notifications.models.notificationsettingprovider import (
121+
NotificationSettingProvider,
122+
)
113123
from sentry.notifications.notifications.base import alert_page_needs_org_id
114124
from sentry.notifications.types import FineTuningAPIKey
115125
from sentry.organizations.services.organization.serial import serialize_rpc_organization
@@ -1162,6 +1172,17 @@ def store_ourlogs(self, ourlogs):
11621172
)
11631173
assert response.status_code == 200
11641174

1175+
def store_occurrences(self, occurrences: Sequence[TraceItem]):
1176+
files = {
1177+
f"occurrence_{i}": occurrence.SerializeToString()
1178+
for i, occurrence in enumerate(occurrences)
1179+
}
1180+
response = requests.post(
1181+
settings.SENTRY_SNUBA + EAP_ITEMS_INSERT_ENDPOINT,
1182+
files=files,
1183+
)
1184+
assert response.status_code == 200
1185+
11651186
def store_trace_metrics(self, trace_metrics):
11661187
files = {
11671188
f"trace_metric_{i}": trace_metric.SerializeToString()
@@ -3488,6 +3509,68 @@ def create_ourlog(
34883509
)
34893510

34903511

3512+
class OccurrenceTestCase(BaseTestCase, TraceItemTestCase):
3513+
def create_eap_occurrence(
3514+
self,
3515+
*,
3516+
organization: Organization | None = None,
3517+
project: Project | None = None,
3518+
group_id: int | None = None,
3519+
event_id: str | None = None,
3520+
trace_id: str | None = None,
3521+
timestamp: datetime | None = None,
3522+
level: str = "error",
3523+
environment: str | None = None,
3524+
title: str = "some error",
3525+
transaction: str | None = None,
3526+
occurrence_type: str = "error",
3527+
tags: dict[str, str] | None = None,
3528+
attributes: dict[str, Any] | None = None,
3529+
retention_days: int = 90,
3530+
) -> TraceItem:
3531+
if organization is None:
3532+
organization = self.organization
3533+
if project is None:
3534+
project = self.project
3535+
if timestamp is None:
3536+
timestamp = datetime.now() - timedelta(minutes=1)
3537+
if event_id is None:
3538+
event_id = uuid4().hex
3539+
if trace_id is None:
3540+
trace_id = uuid4().hex
3541+
3542+
timestamp_proto = Timestamp()
3543+
timestamp_proto.FromDatetime(timestamp)
3544+
3545+
data: dict[str, Any] = {
3546+
"level": level,
3547+
"title": title,
3548+
"type": occurrence_type,
3549+
}
3550+
if group_id is not None:
3551+
data["group_id"] = group_id
3552+
if environment is not None:
3553+
data["environment"] = environment
3554+
if transaction is not None:
3555+
data["transaction"] = transaction
3556+
if attributes:
3557+
data.update(attributes)
3558+
3559+
attributes_proto = _build_occurrence_attributes(data, tags=tags)
3560+
3561+
return TraceItem(
3562+
organization_id=organization.id,
3563+
project_id=project.id,
3564+
item_type=TRACE_ITEM_TYPE_OCCURRENCE,
3565+
timestamp=timestamp_proto,
3566+
trace_id=trace_id,
3567+
item_id=hex_to_item_id(event_id),
3568+
received=timestamp_proto,
3569+
retention_days=retention_days,
3570+
attributes=attributes_proto,
3571+
)
3572+
3573+
34913574
class TraceMetricsTestCase(BaseTestCase, TraceItemTestCase):
34923575
def create_trace_metric(
34933576
self,

tests/sentry/eventstream/test_item_helpers.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sentry.db.models import NodeData
77
from sentry.eventstream.item_helpers import (
88
_ENCODE_MAX_DEPTH,
9-
encode_attributes,
9+
_encode_attributes,
1010
serialize_event_data_as_item,
1111
)
1212
from sentry.services.eventstore.models import Event, GroupEvent
@@ -41,7 +41,7 @@ def test_encode_attributes_basic(self) -> None:
4141
data=event_data,
4242
project_id=self.project.id,
4343
)
44-
result = encode_attributes(event, event_data)
44+
result = _encode_attributes(event, event_data)
4545

4646
assert result["string_field"] == AnyValue(string_value="test")
4747
assert result["int_field"] == AnyValue(int_value=123)
@@ -60,7 +60,7 @@ def test_encode_attributes_with_ignore_fields(self) -> None:
6060
data=event_data,
6161
project_id=self.project.id,
6262
)
63-
result = encode_attributes(event, event_data, ignore_fields={"ignore_field"})
63+
result = _encode_attributes(event, event_data, ignore_fields={"ignore_field"})
6464

6565
assert "keep_field" in result
6666
assert "another_keep" in result
@@ -78,7 +78,7 @@ def test_encode_attributes_with_multiple_ignore_fields(self) -> None:
7878
data=event_data,
7979
project_id=self.project.id,
8080
)
81-
result = encode_attributes(event, event_data, ignore_fields={"field1", "field3"})
81+
result = _encode_attributes(event, event_data, ignore_fields={"field1", "field3"})
8282

8383
assert "field1" not in result
8484
assert "field2" in result
@@ -88,7 +88,7 @@ def test_encode_attributes_with_group_id(self) -> None:
8888
event_data = {"field": "value", "tags": []}
8989

9090
event = self.create_group_event(event_data)
91-
result = encode_attributes(event, event_data)
91+
result = _encode_attributes(event, event_data)
9292

9393
assert result["group_id"] == AnyValue(int_value=event.group.id)
9494
assert result["field"] == AnyValue(string_value="value")
@@ -102,7 +102,7 @@ def test_encode_attributes_without_group_id(self) -> None:
102102
project_id=self.project.id,
103103
)
104104

105-
result = encode_attributes(event, event_data)
105+
result = _encode_attributes(event, event_data)
106106

107107
assert "group_id" not in result
108108
assert result["field"] == AnyValue(string_value="value")
@@ -123,7 +123,7 @@ def test_encode_attributes_with_tags(self) -> None:
123123
project_id=self.project.id,
124124
)
125125

126-
result = encode_attributes(event, event_data)
126+
result = _encode_attributes(event, event_data)
127127

128128
assert result["tags[environment]"] == AnyValue(string_value="production")
129129
assert result["tags[release]"] == AnyValue(string_value="1.0.0")
@@ -147,7 +147,7 @@ def test_encode_attributes_with_empty_tags(self) -> None:
147147
project_id=self.project.id,
148148
)
149149

150-
result = encode_attributes(event, event_data)
150+
result = _encode_attributes(event, event_data)
151151

152152
assert result["field"] == AnyValue(string_value="value")
153153
# No tags[] keys should be present
@@ -164,7 +164,7 @@ def test_encode_attributes_with_integer_tag_values(self) -> None:
164164
}
165165

166166
event = self.create_group_event(event_data)
167-
result = encode_attributes(event, event_data)
167+
result = _encode_attributes(event, event_data)
168168

169169
assert result["tags[numeric_tag]"] == AnyValue(string_value="42")
170170
assert result["tags[string_tag]"] == AnyValue(string_value="value")
@@ -178,12 +178,11 @@ def test_encode_attributes_empty_event_data(self) -> None:
178178
data=event_data,
179179
project_id=self.project.id,
180180
)
181-
result = encode_attributes(event, event_data)
181+
result = _encode_attributes(event, event_data)
182182

183-
# "tags" field itself gets encoded as a non-scalar value in the loop
184-
# Then tags are processed separately, but empty list adds no tag attributes
185-
assert len(result) == 2
186-
assert result["tags"] == AnyValue(array_value=ArrayValue(values=[]))
183+
assert len(result) == 1
184+
assert "tag_keys" in result
185+
assert result["tag_keys"] == AnyValue(array_value=ArrayValue(values=[]))
187186

188187
def test_encode_attributes_with_complex_types(self) -> None:
189188
event_data = {
@@ -198,7 +197,7 @@ def test_encode_attributes_with_complex_types(self) -> None:
198197
project_id=self.project.id,
199198
)
200199

201-
result = encode_attributes(event, event_data)
200+
result = _encode_attributes(event, event_data)
202201

203202
assert result["list_field"] == AnyValue(
204203
array_value=ArrayValue(
@@ -218,7 +217,7 @@ def test_encode_attributes_with_complex_types(self) -> None:
218217
)
219218

220219
def test_encode_attributes_with_none_tags(self) -> None:
221-
"""Test that encode_attributes handles None tags gracefully."""
220+
"""Test that _encode_attributes handles None tags gracefully."""
222221
event_data = {
223222
"field": "value",
224223
"tags": None,
@@ -230,7 +229,7 @@ def test_encode_attributes_with_none_tags(self) -> None:
230229
project_id=self.project.id,
231230
)
232231

233-
result = encode_attributes(event, event_data)
232+
result = _encode_attributes(event, event_data)
234233

235234
assert result["field"] == AnyValue(string_value="value")
236235
# No tags[] keys should be present when tags is None
@@ -239,7 +238,7 @@ def test_encode_attributes_with_none_tags(self) -> None:
239238
assert result["tag_keys"] == AnyValue(array_value=ArrayValue(values=[]))
240239

241240
def test_encode_attributes_with_none_elements_in_tags(self) -> None:
242-
"""Test that encode_attributes handles tags list containing None values."""
241+
"""Test that _encode_attributes handles tags list containing None values."""
243242
event_data = {
244243
"field": "value",
245244
"tags": [
@@ -256,7 +255,7 @@ def test_encode_attributes_with_none_elements_in_tags(self) -> None:
256255
project_id=self.project.id,
257256
)
258257

259-
result = encode_attributes(event, event_data)
258+
result = _encode_attributes(event, event_data)
260259

261260
assert result["field"] == AnyValue(string_value="value")
262261
assert result["tags[tag1]"] == AnyValue(string_value="value1")
@@ -440,7 +439,7 @@ def test_encode_attributes_at_max_depth_boundary(self) -> None:
440439

441440
event_data = {"field": nested, "tags": []}
442441
event = Event(event_id="a" * 32, data=event_data, project_id=self.project.id)
443-
result = encode_attributes(event, event_data)
442+
result = _encode_attributes(event, event_data)
444443

445444
current = result["field"]
446445
for _ in range(_ENCODE_MAX_DEPTH):
@@ -457,7 +456,7 @@ def test_encode_attributes_deeply_nested_list_stringifies(self) -> None:
457456

458457
event_data = {"field": nested, "tags": []}
459458
event = Event(event_id="a" * 32, data=event_data, project_id=self.project.id)
460-
result = encode_attributes(event, event_data)
459+
result = _encode_attributes(event, event_data)
461460

462461
# Walk down to the stringification boundary
463462
current = result["field"]
@@ -476,7 +475,7 @@ def test_encode_attributes_deeply_nested_dict_stringifies(self) -> None:
476475

477476
event_data = {"field": nested, "tags": []}
478477
event = Event(event_id="a" * 32, data=event_data, project_id=self.project.id)
479-
result = encode_attributes(event, event_data)
478+
result = _encode_attributes(event, event_data)
480479

481480
current = result["field"]
482481
for _ in range(_ENCODE_MAX_DEPTH + 1):

0 commit comments

Comments
 (0)