Skip to content

Commit f63e995

Browse files
committed
feat(errors): Add error upsampling aggregation functions with feature flag
Allow projects with error upsampling to use new sample_count(), sample_eps() and sample_epm() function columns in Discover, returning the non extrapolated versions of these functions.
1 parent 7b261d2 commit f63e995

File tree

3 files changed

+179
-22
lines changed

3 files changed

+179
-22
lines changed

src/sentry/api/bases/organization_events.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -435,26 +435,24 @@ def handle_error_upsampling(self, project_ids: Sequence[int], results: dict[str,
435435
data = results.get("data", [])
436436
fields_meta = results.get("meta", {}).get("fields", {})
437437

438-
for result in data:
439-
if "count" in result:
440-
result["count()"] = result["count"]
441-
del result["count"]
442-
if "eps" in result:
443-
result["eps()"] = result["eps"]
444-
del result["eps"]
445-
if "epm" in result:
446-
result["epm()"] = result["epm"]
447-
del result["epm"]
448-
449-
if "count" in fields_meta:
450-
fields_meta["count()"] = fields_meta["count"]
451-
del fields_meta["count"]
452-
if "eps" in fields_meta:
453-
fields_meta["eps()"] = fields_meta["eps"]
454-
del fields_meta["eps"]
455-
if "epm" in fields_meta:
456-
fields_meta["epm()"] = fields_meta["epm"]
457-
del fields_meta["epm"]
438+
upsampling_affected_functions = [
439+
"count",
440+
"eps",
441+
"epm",
442+
"sample_count",
443+
"sample_eps",
444+
"sample_epm",
445+
]
446+
for function in upsampling_affected_functions:
447+
for result in data:
448+
if function in result:
449+
result[f"{function}()"] = result[function]
450+
del result[function]
451+
452+
for function in upsampling_affected_functions:
453+
if function in fields_meta:
454+
fields_meta[f"{function}()"] = fields_meta[function]
455+
del fields_meta[function]
458456

459457
def handle_issues(
460458
self, results: Sequence[Any], project_ids: Sequence[int], organization: Organization

src/sentry/api/helpers/error_upsampling.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,16 @@ def transform_query_columns_for_error_upsampling(query_columns: Sequence[str]) -
5555

5656
if column_lower == "count()":
5757
transformed_columns.append("upsampled_count() as count")
58-
5958
elif column_lower == "eps()":
6059
transformed_columns.append("upsampled_eps() as eps")
61-
6260
elif column_lower == "epm()":
6361
transformed_columns.append("upsampled_epm() as epm")
62+
elif column_lower == "sample_count()":
63+
transformed_columns.append("count() as sample_count")
64+
elif column_lower == "sample_eps()":
65+
transformed_columns.append("eps() as sample_eps")
66+
elif column_lower == "sample_epm()":
67+
transformed_columns.append("epm() as sample_epm")
6468
else:
6569
transformed_columns.append(column)
6670

tests/snuba/api/endpoints/test_organization_events.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6822,6 +6822,161 @@ def test_error_upsampling_with_partial_allowlist(self):
68226822
# Expect upsampling since any project is allowlisted (both events upsampled: 10 + 10 = 20)
68236823
assert response.data["data"][0]["count()"] == 20
68246824

6825+
def test_sample_count_with_allowlisted_project(self):
6826+
"""Test that sample_count() returns raw sample count (not upsampled) for allowlisted projects."""
6827+
# Set up allowlisted project
6828+
with self.options({"issues.client_error_sampling.project_allowlist": [self.project.id]}):
6829+
# Store error event with error_sampling context
6830+
self.store_event(
6831+
data={
6832+
"event_id": "a" * 32,
6833+
"message": "Error event for sample_count",
6834+
"type": "error",
6835+
"exception": [{"type": "ValueError", "value": "Something went wrong"}],
6836+
"timestamp": self.ten_mins_ago_iso,
6837+
"fingerprint": ["group1"],
6838+
"contexts": {"error_sampling": {"client_sample_rate": 0.1}},
6839+
},
6840+
project_id=self.project.id,
6841+
)
6842+
6843+
# Store error event without error_sampling context (sample_weight = null should count as 1)
6844+
self.store_event(
6845+
data={
6846+
"event_id": "a1" * 16,
6847+
"message": "Error event without sampling",
6848+
"type": "error",
6849+
"exception": [{"type": "ValueError", "value": "Something else went wrong"}],
6850+
"timestamp": self.ten_mins_ago_iso,
6851+
"fingerprint": ["group1_no_sampling"],
6852+
},
6853+
project_id=self.project.id,
6854+
)
6855+
6856+
# Test with errors dataset - sample_count() should return raw count, not upsampled
6857+
query = {
6858+
"field": ["sample_count()"],
6859+
"statsPeriod": "2h",
6860+
"query": "event.type:error",
6861+
"dataset": "errors",
6862+
}
6863+
response = self.do_request(query)
6864+
assert response.status_code == 200, response.content
6865+
# Expect sample_count to return raw count: 2 events (not upsampled 11)
6866+
assert response.data["data"][0]["sample_count()"] == 2
6867+
6868+
# Check meta information
6869+
meta = response.data["meta"]
6870+
assert "fields" in meta
6871+
assert "sample_count()" in meta["fields"]
6872+
assert meta["fields"]["sample_count()"] == "integer"
6873+
6874+
def test_sample_eps_with_allowlisted_project(self):
6875+
"""Test that sample_eps() returns raw sample rate (not upsampled) for allowlisted projects."""
6876+
# Set up allowlisted project
6877+
with self.options({"issues.client_error_sampling.project_allowlist": [self.project.id]}):
6878+
# Store error event with error_sampling context
6879+
self.store_event(
6880+
data={
6881+
"event_id": "b" * 32,
6882+
"message": "Error event for sample_eps",
6883+
"type": "error",
6884+
"exception": [{"type": "ValueError", "value": "Something went wrong"}],
6885+
"timestamp": self.ten_mins_ago_iso,
6886+
"fingerprint": ["group2"],
6887+
"contexts": {"error_sampling": {"client_sample_rate": 0.1}},
6888+
},
6889+
project_id=self.project.id,
6890+
)
6891+
6892+
# Store error event without error_sampling context (sample_weight = null should count as 1)
6893+
self.store_event(
6894+
data={
6895+
"event_id": "b1" * 16,
6896+
"message": "Error event without sampling for sample_eps",
6897+
"type": "error",
6898+
"exception": [{"type": "ValueError", "value": "Something else went wrong"}],
6899+
"timestamp": self.ten_mins_ago_iso,
6900+
"fingerprint": ["group2_no_sampling"],
6901+
},
6902+
project_id=self.project.id,
6903+
)
6904+
6905+
# Test with errors dataset - sample_eps() should return raw rate, not upsampled
6906+
query = {
6907+
"field": ["sample_eps()"],
6908+
"statsPeriod": "2h",
6909+
"query": "event.type:error",
6910+
"dataset": "errors",
6911+
}
6912+
response = self.do_request(query)
6913+
assert response.status_code == 200, response.content
6914+
# Expect sample_eps to return raw rate: 2 events / 7200 seconds = 2/7200
6915+
expected_sample_eps = 2 / 7200
6916+
actual_sample_eps = response.data["data"][0]["sample_eps()"]
6917+
assert (
6918+
abs(actual_sample_eps - expected_sample_eps) < 0.0001
6919+
) # Allow small rounding differences
6920+
6921+
# Check meta information
6922+
meta = response.data["meta"]
6923+
assert "fields" in meta
6924+
assert "sample_eps()" in meta["fields"]
6925+
assert meta["fields"]["sample_eps()"] == "rate"
6926+
6927+
def test_sample_epm_with_allowlisted_project(self):
6928+
"""Test that sample_epm() returns raw sample rate (not upsampled) for allowlisted projects."""
6929+
# Set up allowlisted project
6930+
with self.options({"issues.client_error_sampling.project_allowlist": [self.project.id]}):
6931+
# Store error event with error_sampling context
6932+
self.store_event(
6933+
data={
6934+
"event_id": "c" * 32,
6935+
"message": "Error event for sample_epm",
6936+
"type": "error",
6937+
"exception": [{"type": "ValueError", "value": "Something went wrong"}],
6938+
"timestamp": self.ten_mins_ago_iso,
6939+
"fingerprint": ["group3"],
6940+
"contexts": {"error_sampling": {"client_sample_rate": 0.1}},
6941+
},
6942+
project_id=self.project.id,
6943+
)
6944+
6945+
# Store error event without error_sampling context (sample_weight = null should count as 1)
6946+
self.store_event(
6947+
data={
6948+
"event_id": "c1" * 16,
6949+
"message": "Error event without sampling for sample_epm",
6950+
"type": "error",
6951+
"exception": [{"type": "ValueError", "value": "Something else went wrong"}],
6952+
"timestamp": self.ten_mins_ago_iso,
6953+
"fingerprint": ["group3_no_sampling"],
6954+
},
6955+
project_id=self.project.id,
6956+
)
6957+
6958+
# Test with errors dataset - sample_epm() should return raw rate, not upsampled
6959+
query = {
6960+
"field": ["sample_epm()"],
6961+
"statsPeriod": "2h",
6962+
"query": "event.type:error",
6963+
"dataset": "errors",
6964+
}
6965+
response = self.do_request(query)
6966+
assert response.status_code == 200, response.content
6967+
# Expect sample_epm to return raw rate: 2 events / 120 minutes = 2/120
6968+
expected_sample_epm = 2 / 120
6969+
actual_sample_epm = response.data["data"][0]["sample_epm()"]
6970+
assert (
6971+
abs(actual_sample_epm - expected_sample_epm) < 0.001
6972+
) # Allow small rounding differences
6973+
6974+
# Check meta information
6975+
meta = response.data["meta"]
6976+
assert "fields" in meta
6977+
assert "sample_epm()" in meta["fields"]
6978+
assert meta["fields"]["sample_epm()"] == "rate"
6979+
68256980
def test_is_status(self):
68266981
self.store_event(
68276982
data={

0 commit comments

Comments
 (0)