feat(upsampling) - Support upsampled error count with performance optimizations - #8
feat(upsampling) - Support upsampled error count with performance optimizations#8CodingKylo wants to merge 2 commits into
Conversation
…(#94376) Part of the Error Upsampling project: https://www.notion.so/sentry/Tech-Spec-Error-Up-Sampling-1e58b10e4b5d80af855cf3b992f75894?source=copy_link Events-stats API will now check if all projects in the query are allowlisted for upsampling, and convert the count query to a sum over `sample_weight` in Snuba, this is done by defining a new SnQL function `upsampled_count()`. I noticed there are also eps() and epm() functions in use in this endpoint. I considered (and even worked on) also supporting swapping eps() and epm() which for correctness should probably also not count naively and use `sample_weight`, but this caused some complications and since they are only in use by specific dashboard widgets and not available in discover I decided to defer changing them until we realize it is needed.
- Add 60-second cache for upsampling eligibility checks to improve performance - Separate upsampling eligibility check from query transformation for better optimization - Remove unnecessary null checks in upsampled_count() function per schema requirements - Add cache invalidation utilities for configuration management This improves performance during high-traffic periods by avoiding repeated expensive allowlist lookups while maintaining data consistency.
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🚨 Risk Score: 76/100 · CRITICAL
| Dimension | Level |
|---|---|
| Likelihood | 🔴 Critical |
| Impact | 🔴 Critical |
| Detectability | 🟡 Medium |
Intent
Add an optimized “error upsampling” path so organization event stats can report upsampled error counts using sample-weighted aggregation for allowlisted projects.
Summary
The PR introduces a new helper module to decide whether to apply error upsampling, adds a new SnQL aggregation function upsampled_count that computes sum(sample_weight), and wires an early eligibility check into the organization events stats endpoint. The biggest risks are (1) a contract mismatch: upsampled_count assumes sample_weight is present, but the test factory changes appear to inject sample_rate from contexts.error_sampling rather than sample_weight, which could silently yield 0/NULL or runtime query failures. (2) eligibility/gating consistency: the endpoint performs an early cached eligibility decision, while the helper also applies additional gating via _should_apply_sample_weight_transform, so any mismatch can produce inconsistent results across code paths. Please verify end-to-end that the same conditions that make the helper eligible also guarantee sample_weight exists in the Snuba dataset rows and that the query transformation uses the same gating logic everywhere.
🎯 Review Focus
Verify the end-to-end contract for sample_weight: under the exact eligibility conditions used by the endpoint/helper, confirm that Snuba rows include sample_weight and that the generated SnQL uses upsampled_count (sum(sample_weight))—otherwise metrics will be silently wrong or the query will fail.
Key Findings
- 🚨 [src/sentry/search/events/datasets/discover.py:L1038] CRITICAL:
upsampled_counthard-depends onsample_weightwithout any guard/validation —sum(sample_weight)can return NULL/0 or break the query ifsample_weightis not present in the dataset for the eligible cases. Quote:snql_aggregate=lambda args, alias: Function( "toInt64", [Function("sum", [Column("sample_weight")])], alias, ),— Why: this is a production metrics endpoint; ifsample_weightis missing for some events/projects (or only produced under different normalization than the tests), the endpoint will silently report incorrect counts or fail at query execution. Fix: ensure the dataset always materializessample_weightwhen error upsampling is enabled, or add a safe fallback in SnQL (e.g.,coalesce(sample_weight, 0)), and add an integration test that asserts the Snuba query includessample_weightand returns non-zero results for an allowlisted error-focused query. Example SnQL change:Function("sum", [Function("coalesce", [Column("sample_weight"), Literal(0)])])(or equivalent supported SnQL construct), plus an integration test that fails ifsample_weightis absent. - 🚨 [src/sentry/api/helpers/error_upsampling.py:L18] CRITICAL: cached eligibility can become stale and produce incorrect metrics for up to 60 seconds, and the cache key does not include request-level gating inputs (only org id + project_ids). Quote:
cache_key = f"error_upsampling_eligible:{organization.id}:{hash(tuple(sorted(snuba_params.project_ids)))}"andcache.set(cache_key, is_eligible, 60)— Why: the helper returnsis_eligible and _should_apply_sample_weight_transform(dataset, request), but the cached value only storesis_eligible. If_should_apply_sample_weight_transform(dataset, request)depends on request parameters (e.g., query shape, time range, dataset type), caching only the allowlist portion is fine only if that request-dependent gating is always recomputed (it is recomputed in the cached branch, but the cache key omission is still a correctness risk if_should_apply_sample_weight_transformever changes to depend on more than dataset/request in a way not reflected). Fix: either (a) remove caching entirely or (b) include all request-dependent gating inputs in the cache key (or cache only the allowlist check and keep request gating strictly outside the cached value, with a comment + test that proves request gating is always recomputed). Concretely: change the cache to store only_are_all_projects_error_upsampled(...)and never mix it with request gating; add a unit test that varies request query parameters and asserts the final decision changes even when the allowlist cache hits.
✅ Action Checklist
- [ ] CRITICAL [src/sentry/search/events/datasets/discover.py:L1038] —
upsampled_counthard-depends onsample_weightwithout any guard/validation —sum(sample_weight)can return NULL/0 or break the query ifsample_weightis not present in the dataset for the eligible cases. Quote:snql_aggregate=lambda args, alias: Function( "toInt64", [Function("sum", [Column("sample_weight")])], alias, ),— Why: this is a production metrics endpoint; ifsample_weightis missing for some events/projects (or only produced under different normalization than the tests), the endpoint will silently report incorrect counts or fail at query execution. Fix: ensure the dataset always materializessample_weightwhen error upsampling is enabled, or add a safe fallback in SnQL (e.g.,coalesce(sample_weight, 0)), and add an integration test that asserts the Snuba query includessample_weightand returns non-zero results for an allowlisted error-focused query. Example SnQL change:Function("sum", [Function("coalesce", [Column("sample_weight"), Literal(0)])])(or equivalent supported SnQL construct), plus an integration test that fails ifsample_weightis absent. - [ ] CRITICAL [src/sentry/api/helpers/error_upsampling.py:L18] — cached eligibility can become stale and produce incorrect metrics for up to 60 seconds, and the cache key does not include request-level gating inputs (only org id + project_ids). Quote:
cache_key = f"error_upsampling_eligible:{organization.id}:{hash(tuple(sorted(snuba_params.project_ids)))}"andcache.set(cache_key, is_eligible, 60)— Why: the helper returnsis_eligible and _should_apply_sample_weight_transform(dataset, request), but the cached value only storesis_eligible. If_should_apply_sample_weight_transform(dataset, request)depends on request parameters (e.g., query shape, time range, dataset type), caching only the allowlist portion is fine only if that request-dependent gating is always recomputed (it is recomputed in the cached branch, but the cache key omission is still a correctness risk if_should_apply_sample_weight_transformever changes to depend on more than dataset/request in a way not reflected). Fix: either (a) remove caching entirely or (b) include all request-dependent gating inputs in the cache key (or cache only the allowlist check and keep request gating strictly outside the cached value, with a comment + test that proves request gating is always recomputed). Concretely: change the cache to store only_are_all_projects_error_upsampled(...)and never mix it with request gating; add a unit test that varies request query parameters and asserts the final decision changes even when the allowlist cache hits. - [ ] SUGGESTION — [src/sentry/api/endpoints/organization_events_stats.py:L215] WARNING: endpoint introduces a second “source of truth” for eligibility (
should_upsample/upsampling_enabled) but the diff is truncated and it’s unclear whether the later query transformation uses the same gating function as the helper. Quote:should_upsample = is_errors_query_for_error_upsampled_projects(...); upsampling_enabled = should_upsample; final_columns = query_columns— Why: if later code transforms columns based onupsampling_enabledwhile the helper also applies_should_apply_sample_weight_transform(dataset, request), any mismatch will yield inconsistent results across query-building paths. Fix: centralize gating by having the endpoint call onlytransform_query_columns_for_error_upsampling(...)and let that function decide internally; removeupsampling_enabledplumbing unless it is strictly redundant. Add a test that asserts the transformed SnQL usesupsampled_countexactly when_should_apply_sample_weight_transformwould return true. - [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py:L44-L74] WARNING: error-focused query detection relies on a brittle substring heuristic (
"event.type:error" in request.GET['query']). Quote (from risk evidence):_is_error_focused_query checks only if "event.type:error" substring is present in request.GET['query']. — Why: SnQL query formatting can vary (whitespace, parentheses, aliases, URL encoding, different field names), causing eligibility to be applied/withheld incorrectly and therefore changing metrics semantics. Fix: parse the SnQL query AST (or at least normalize the query string) and detect error-focused intent structurally. If AST parsing isn’t available, implement robust normalization: URL-decoding, whitespace normalization, and case/field-name normalization, plus tests for common query variants. - [ ] SUGGESTION — [src/sentry/testutils/factories.py:L341-L365] WARNING: test factory injects
sample_ratefromcontexts.error_sampling.client_sample_rate, but the new aggregation usessample_weight. Quote (from risk evidence):_set_sample_rate_from_error_sampling and calls it from store_event.— Why: if production normalization mapsclient_sample_rate→sample_weightdifferently (or not at all), unit tests may pass while production returns 0/NULL. Fix: align test event injection with the real pipeline: ensure the factory produces whatever field the Snuba dataset uses (sample_weight) or add an integration test that assertssample_weightis present in the Snuba rows for eligible queries. - [ ] SUGGESTION — [tests/sentry/api/helpers/test_error_upsampling.py:L1-L101] INFO: unit tests appear to import and test private helpers and mock options, which can mask integration failures around Snuba schema/fields. Quote (from risk evidence): tests cover
_are_all_projects_error_upsampled,_is_error_focused_query,_should_apply_sample_weight_transform. — Fix: add one end-to-end test that runs the endpoint against Snuba (or a realistic dataset fixture) and asserts the response changes specifically becauseupsampled_countis used (e.g., verify non-zero and/or verify the generated SnQL containssum(sample_weight)).
Suggestions
- [src/sentry/api/endpoints/organization_events_stats.py:L215] WARNING: endpoint introduces a second “source of truth” for eligibility (
should_upsample/upsampling_enabled) but the diff is truncated and it’s unclear whether the later query transformation uses the same gating function as the helper. Quote:should_upsample = is_errors_query_for_error_upsampled_projects(...); upsampling_enabled = should_upsample; final_columns = query_columns— Why: if later code transforms columns based onupsampling_enabledwhile the helper also applies_should_apply_sample_weight_transform(dataset, request), any mismatch will yield inconsistent results across query-building paths. Fix: centralize gating by having the endpoint call onlytransform_query_columns_for_error_upsampling(...)and let that function decide internally; removeupsampling_enabledplumbing unless it is strictly redundant. Add a test that asserts the transformed SnQL usesupsampled_countexactly when_should_apply_sample_weight_transformwould return true. - [src/sentry/api/helpers/error_upsampling.py:L44-L74] WARNING: error-focused query detection relies on a brittle substring heuristic (
"event.type:error" in request.GET['query']). Quote (from risk evidence):_is_error_focused_query checks only if "event.type:error" substring is present in request.GET['query']. — Why: SnQL query formatting can vary (whitespace, parentheses, aliases, URL encoding, different field names), causing eligibility to be applied/withheld incorrectly and therefore changing metrics semantics. Fix: parse the SnQL query AST (or at least normalize the query string) and detect error-focused intent structurally. If AST parsing isn’t available, implement robust normalization: URL-decoding, whitespace normalization, and case/field-name normalization, plus tests for common query variants. - [src/sentry/testutils/factories.py:L341-L365] WARNING: test factory injects
sample_ratefromcontexts.error_sampling.client_sample_rate, but the new aggregation usessample_weight. Quote (from risk evidence):_set_sample_rate_from_error_sampling and calls it from store_event.— Why: if production normalization mapsclient_sample_rate→sample_weightdifferently (or not at all), unit tests may pass while production returns 0/NULL. Fix: align test event injection with the real pipeline: ensure the factory produces whatever field the Snuba dataset uses (sample_weight) or add an integration test that assertssample_weightis present in the Snuba rows for eligible queries. - [tests/sentry/api/helpers/test_error_upsampling.py:L1-L101] INFO: unit tests appear to import and test private helpers and mock options, which can mask integration failures around Snuba schema/fields. Quote (from risk evidence): tests cover
_are_all_projects_error_upsampled,_is_error_focused_query,_should_apply_sample_weight_transform. — Fix: add one end-to-end test that runs the endpoint against Snuba (or a realistic dataset fixture) and asserts the response changes specifically becauseupsampled_countis used (e.g., verify non-zero and/or verify the generated SnQL containssum(sample_weight)).
Posted by re-entry.ai · Risk governance for autonomous engineering teams
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🚨 Risk Score: 76/100 · CRITICAL
| Dimension | Level |
|---|---|
| Likelihood | 🔴 Critical |
| Impact | 🔴 Critical |
| Detectability | 🟡 Medium |
Intent
Add an optimized “error upsampling” path so organization event stats can report upsampled error counts for allowlisted projects, using a cached eligibility check and a new SnQL aggregation.
Summary
The PR introduces a new helper module that decides whether to apply error upsampling based on project allowlist membership and a query-string heuristic, then wires that decision into the organization events stats endpoint. It also adds a new SnQL aggregation function upsampled_count implemented as sum(sample_weight) and modifies query-building to use it. The two biggest risks to verify are (1) correctness of the eligibility heuristic and caching key (wrong eligibility will change production metrics), and (2) the hard assumption that sample_weight exists for all events when upsampled_count is used (otherwise queries can error or silently miscount). You should also verify end-to-end wiring: unit tests may not exercise the full event normalization path that produces sample_weight.
🎯 Review Focus
Confirm the end-to-end contract between eligibility decision, query transformation, and the presence of sample_weight: when upsampled_count is used, the query must also guarantee sample_weight exists for every row, and the eligibility cache key must reflect the actual resolved project set for the request.
Key Findings
- 🚨 [src/sentry/api/helpers/error_upsampling.py:L18-L33] CRITICAL:
cache_keyis derived fromsnuba_params.project_idswithout guaranteeing it is present/stable:hash(tuple(sorted(snuba_params.project_ids)))— Ifproject_idsis missing, empty, or not deterministically populated for a given request, the cache can return eligibility for the wrong project set, causing the endpoint to apply upsampling transformations incorrectly and permanently skew metrics for that request. Fix: compute a deterministic project-id set in the endpoint (or in the helper) from the resolved Snuba dataset query, and fail closed when it’s unavailable. Example: in the helper, guardif not snuba_params.project_ids: return False(or compute fromdataset/organization), and include all relevant dimensions in the cache key (e.g., dataset name + resolved project ids). - 🚨 [src/sentry/search/events/datasets/discover.py:L1038-L1056] CRITICAL:
upsampled_counthard-assumessample_weightexists for all events:Function("sum", [Column("sample_weight")])— If upstream events lacksample_weight(or the transform gate is wrong), Snuba/ClickHouse can error or return incorrect results (e.g., NULL propagation or missing column behavior), breaking the endpoint or silently corrupting counts. Fix: make the aggregation robust to missing weights, e.g.sum(ifNull(sample_weight, 0))(or ClickHouse equivalentsum(coalesce(sample_weight, 0))) and/or enforce intransform_query_columns_for_error_upsamplingthatsample_weightis always produced wheneverupsampled_countis used. Concretely, change the lambda toFunction("sum", [Function("ifNull", [Column("sample_weight"), Literal(0)])])(adjust to the SnQL function set available). ⚠️ [src/sentry/api/endpoints/organization_events_stats.py:L211-L215] WARNING: eligibility is computed early and then “applied later during query building” but the diff shows no guarantee that the same eligibility decision is used when constructing the final query columns:should_upsample = is_errors_query_for_error_upsampled_projects(...); upsampling_enabled = should_upsample; final_columns = query_columns— If later code paths mutatequery_columns/dataset selection or ifrequest.GET['query']parsing differs from the actual Snuba query, you can end up withupsampled_countbeing used without the correspondingsample_weighttransform (or vice versa), producing wrong metrics or runtime failures. Fix: threadupsampling_enabledexplicitly into the exact query transformation function that addsupsampled_countand thesample_weight-producing transform, and add an assertion/test that whenupsampled_countis present, the query also includes thesample_weighttransform.⚠️ [src/sentry/api/helpers/error_upsampling.py:L44-L74] WARNING: the “error-focused query” heuristic is a substring check on the raw query string:_is_error_focused_querychecks only if"event.type:error"is present inrequest.GET['query']— This is brittle: equivalent SnQL queries (different spacing/casing/aliases, or usingevent.type = "error") won’t match, so upsampling may not apply when it should (or apply when the substring appears in an unrelated context). Fix: determine “error-focused” from the parsed Snuba/SnQL AST or from the normalized query representation used to build the dataset, not from the rawrequest.GET['query']string. If AST parsing isn’t available, at least normalize (lowercase, strip whitespace) and support common variants (e.g.,event.type:error,event.type = error,event.type:"error").
✅ Action Checklist
- [ ] CRITICAL [src/sentry/api/helpers/error_upsampling.py:L18-L33] —
cache_keyis derived fromsnuba_params.project_idswithout guaranteeing it is present/stable:hash(tuple(sorted(snuba_params.project_ids)))— Ifproject_idsis missing, empty, or not deterministically populated for a given request, the cache can return eligibility for the wrong project set, causing the endpoint to apply upsampling transformations incorrectly and permanently skew metrics for that request. Fix: compute a deterministic project-id set in the endpoint (or in the helper) from the resolved Snuba dataset query, and fail closed when it’s unavailable. Example: in the helper, guardif not snuba_params.project_ids: return False(or compute fromdataset/organization), and include all relevant dimensions in the cache key (e.g., dataset name + resolved project ids). - [ ] CRITICAL [src/sentry/search/events/datasets/discover.py:L1038-L1056] —
upsampled_counthard-assumessample_weightexists for all events:Function("sum", [Column("sample_weight")])— If upstream events lacksample_weight(or the transform gate is wrong), Snuba/ClickHouse can error or return incorrect results (e.g., NULL propagation or missing column behavior), breaking the endpoint or silently corrupting counts. Fix: make the aggregation robust to missing weights, e.g.sum(ifNull(sample_weight, 0))(or ClickHouse equivalentsum(coalesce(sample_weight, 0))) and/or enforce intransform_query_columns_for_error_upsamplingthatsample_weightis always produced wheneverupsampled_countis used. Concretely, change the lambda toFunction("sum", [Function("ifNull", [Column("sample_weight"), Literal(0)])])(adjust to the SnQL function set available). - [ ] WARNING [src/sentry/api/endpoints/organization_events_stats.py:L211-L215] — eligibility is computed early and then “applied later during query building” but the diff shows no guarantee that the same eligibility decision is used when constructing the final query columns:
should_upsample = is_errors_query_for_error_upsampled_projects(...); upsampling_enabled = should_upsample; final_columns = query_columns— If later code paths mutatequery_columns/dataset selection or ifrequest.GET['query']parsing differs from the actual Snuba query, you can end up withupsampled_countbeing used without the correspondingsample_weighttransform (or vice versa), producing wrong metrics or runtime failures. Fix: threadupsampling_enabledexplicitly into the exact query transformation function that addsupsampled_countand thesample_weight-producing transform, and add an assertion/test that whenupsampled_countis present, the query also includes thesample_weighttransform. - [ ] WARNING [src/sentry/api/helpers/error_upsampling.py:L44-L74] — the “error-focused query” heuristic is a substring check on the raw query string:
_is_error_focused_querychecks only if"event.type:error"is present inrequest.GET['query']— This is brittle: equivalent SnQL queries (different spacing/casing/aliases, or usingevent.type = "error") won’t match, so upsampling may not apply when it should (or apply when the substring appears in an unrelated context). Fix: determine “error-focused” from the parsed Snuba/SnQL AST or from the normalized query representation used to build the dataset, not from the rawrequest.GET['query']string. If AST parsing isn’t available, at least normalize (lowercase, strip whitespace) and support common variants (e.g.,event.type:error,event.type = error,event.type:"error"). - [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py:L18-L33] Add a “fail closed” guard and a test: if
snuba_params.project_idsis empty/None, return False (no upsampling) rather than hashing an empty tuple and caching a potentially wrong eligibility. Then add a unit test for the “projects provided vs projects resolved” scenario to ensure the cache key matches the actual resolved project set. - [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py] Verify cache invalidation wiring: the helper caches for 60 seconds (
cache.set(cache_key, is_eligible, 60)) but the diff doesn’t showinvalidate_upsampling_cachebeing called on allowlist changes. Fix: ensure allowlist update code paths call invalidation, or reduce TTL and/or include an allowlist version in the cache key (e.g.,options.get(...).updated_ator a monotonically increasing config revision). - [ ] SUGGESTION — [src/sentry/api/endpoints/organization_events_stats.py] Add an end-to-end test that stores events and asserts the generated Snuba query uses
upsampled_countAND includes thesample_weighttransform. Current tests may only validate helper logic and not the full event normalization path that producessample_weight(see factory changes insrc/sentry/testutils/factories.py). - [ ] SUGGESTION — [src/sentry/api/endpoints/organization_events_stats.py] Run the full
tests/sentry/api/endpoints/test_organization_events_stats.pysuite and check for brittle assertions around error messages/response shapes. The diff changes the validation detail string ("If topEvents needs to be at least 1"->"topEvents needs to be at least 1"), which can break unrelated tests if they assert exact strings.
Suggestions
- [src/sentry/api/helpers/error_upsampling.py:L18-L33] Add a “fail closed” guard and a test: if
snuba_params.project_idsis empty/None, return False (no upsampling) rather than hashing an empty tuple and caching a potentially wrong eligibility. Then add a unit test for the “projects provided vs projects resolved” scenario to ensure the cache key matches the actual resolved project set. - [src/sentry/api/helpers/error_upsampling.py] Verify cache invalidation wiring: the helper caches for 60 seconds (
cache.set(cache_key, is_eligible, 60)) but the diff doesn’t showinvalidate_upsampling_cachebeing called on allowlist changes. Fix: ensure allowlist update code paths call invalidation, or reduce TTL and/or include an allowlist version in the cache key (e.g.,options.get(...).updated_ator a monotonically increasing config revision). - [src/sentry/api/endpoints/organization_events_stats.py] Add an end-to-end test that stores events and asserts the generated Snuba query uses
upsampled_countAND includes thesample_weighttransform. Current tests may only validate helper logic and not the full event normalization path that producessample_weight(see factory changes insrc/sentry/testutils/factories.py). - [src/sentry/api/endpoints/organization_events_stats.py] Run the full
tests/sentry/api/endpoints/test_organization_events_stats.pysuite and check for brittle assertions around error messages/response shapes. The diff changes the validation detail string ("If topEvents needs to be at least 1"->"topEvents needs to be at least 1"), which can break unrelated tests if they assert exact strings.
Posted by re-entry.ai · Risk governance for autonomous engineering teams
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🚨 Risk Score: 76/100 · CRITICAL
| Dimension | Level |
|---|---|
| Likelihood | 🔴 Critical |
| Impact | 🔴 Critical |
| Detectability | 🟡 Medium |
Intent
Add an optimized “error upsampling” path so organization event stats can report upsampled error counts for allowlisted projects, using a cached eligibility check and a new SnQL aggregation.
Summary
The PR introduces a new helper module that decides whether to apply error upsampling based on project allowlist membership and a query-string heuristic, then wires that decision into the organization events stats endpoint. It also adds a new SnQL aggregation function upsampled_count implemented as sum(sample_weight) and modifies query-building to use it. The two biggest risks to verify are (1) correctness of the eligibility heuristic and caching key (wrong eligibility will change production metrics), and (2) the hard assumption that sample_weight exists for all events when upsampled_count is used (otherwise queries can error or silently miscount). You should also verify end-to-end wiring: unit tests may not exercise the full event normalization path that produces sample_weight.
🎯 Review Focus
Confirm the end-to-end contract between eligibility decision, query transformation, and the presence of sample_weight: when upsampled_count is used, the query must also guarantee sample_weight exists for every row, and the eligibility cache key must reflect the actual resolved project set for the request.
Key Findings
- 🚨 [src/sentry/api/helpers/error_upsampling.py:L18-L33] CRITICAL:
cache_keyis derived fromsnuba_params.project_idswithout guaranteeing it is present/stable:hash(tuple(sorted(snuba_params.project_ids)))— Ifproject_idsis missing, empty, or not deterministically populated for a given request, the cache can return eligibility for the wrong project set, causing the endpoint to apply upsampling transformations incorrectly and permanently skew metrics for that request. Fix: compute a deterministic project-id set in the endpoint (or in the helper) from the resolved Snuba dataset query, and fail closed when it’s unavailable. Example: in the helper, guardif not snuba_params.project_ids: return False(or compute fromdataset/organization), and include all relevant dimensions in the cache key (e.g., dataset name + resolved project ids). - 🚨 [src/sentry/search/events/datasets/discover.py:L1038-L1056] CRITICAL:
upsampled_counthard-assumessample_weightexists for all events:Function("sum", [Column("sample_weight")])— If upstream events lacksample_weight(or the transform gate is wrong), Snuba/ClickHouse can error or return incorrect results (e.g., NULL propagation or missing column behavior), breaking the endpoint or silently corrupting counts. Fix: make the aggregation robust to missing weights, e.g.sum(ifNull(sample_weight, 0))(or ClickHouse equivalentsum(coalesce(sample_weight, 0))) and/or enforce intransform_query_columns_for_error_upsamplingthatsample_weightis always produced wheneverupsampled_countis used. Concretely, change the lambda toFunction("sum", [Function("ifNull", [Column("sample_weight"), Literal(0)])])(adjust to the SnQL function set available). ⚠️ [src/sentry/api/endpoints/organization_events_stats.py:L211-L215] WARNING: eligibility is computed early and then “applied later during query building” but the diff shows no guarantee that the same eligibility decision is used when constructing the final query columns:should_upsample = is_errors_query_for_error_upsampled_projects(...); upsampling_enabled = should_upsample; final_columns = query_columns— If later code paths mutatequery_columns/dataset selection or ifrequest.GET['query']parsing differs from the actual Snuba query, you can end up withupsampled_countbeing used without the correspondingsample_weighttransform (or vice versa), producing wrong metrics or runtime failures. Fix: threadupsampling_enabledexplicitly into the exact query transformation function that addsupsampled_countand thesample_weight-producing transform, and add an assertion/test that whenupsampled_countis present, the query also includes thesample_weighttransform.⚠️ [src/sentry/api/helpers/error_upsampling.py:L44-L74] WARNING: the “error-focused query” heuristic is a substring check on the raw query string:_is_error_focused_querychecks only if"event.type:error"is present inrequest.GET['query']— This is brittle: equivalent SnQL queries (different spacing/casing/aliases, or usingevent.type = "error") won’t match, so upsampling may not apply when it should (or apply when the substring appears in an unrelated context). Fix: determine “error-focused” from the parsed Snuba/SnQL AST or from the normalized query representation used to build the dataset, not from the rawrequest.GET['query']string. If AST parsing isn’t available, at least normalize (lowercase, strip whitespace) and support common variants (e.g.,event.type:error,event.type = error,event.type:"error").
✅ Action Checklist
- [ ] CRITICAL [src/sentry/api/helpers/error_upsampling.py:L18-L33] —
cache_keyis derived fromsnuba_params.project_idswithout guaranteeing it is present/stable:hash(tuple(sorted(snuba_params.project_ids)))— Ifproject_idsis missing, empty, or not deterministically populated for a given request, the cache can return eligibility for the wrong project set, causing the endpoint to apply upsampling transformations incorrectly and permanently skew metrics for that request. Fix: compute a deterministic project-id set in the endpoint (or in the helper) from the resolved Snuba dataset query, and fail closed when it’s unavailable. Example: in the helper, guardif not snuba_params.project_ids: return False(or compute fromdataset/organization), and include all relevant dimensions in the cache key (e.g., dataset name + resolved project ids). - [ ] CRITICAL [src/sentry/search/events/datasets/discover.py:L1038-L1056] —
upsampled_counthard-assumessample_weightexists for all events:Function("sum", [Column("sample_weight")])— If upstream events lacksample_weight(or the transform gate is wrong), Snuba/ClickHouse can error or return incorrect results (e.g., NULL propagation or missing column behavior), breaking the endpoint or silently corrupting counts. Fix: make the aggregation robust to missing weights, e.g.sum(ifNull(sample_weight, 0))(or ClickHouse equivalentsum(coalesce(sample_weight, 0))) and/or enforce intransform_query_columns_for_error_upsamplingthatsample_weightis always produced wheneverupsampled_countis used. Concretely, change the lambda toFunction("sum", [Function("ifNull", [Column("sample_weight"), Literal(0)])])(adjust to the SnQL function set available). - [ ] WARNING [src/sentry/api/endpoints/organization_events_stats.py:L211-L215] — eligibility is computed early and then “applied later during query building” but the diff shows no guarantee that the same eligibility decision is used when constructing the final query columns:
should_upsample = is_errors_query_for_error_upsampled_projects(...); upsampling_enabled = should_upsample; final_columns = query_columns— If later code paths mutatequery_columns/dataset selection or ifrequest.GET['query']parsing differs from the actual Snuba query, you can end up withupsampled_countbeing used without the correspondingsample_weighttransform (or vice versa), producing wrong metrics or runtime failures. Fix: threadupsampling_enabledexplicitly into the exact query transformation function that addsupsampled_countand thesample_weight-producing transform, and add an assertion/test that whenupsampled_countis present, the query also includes thesample_weighttransform. - [ ] WARNING [src/sentry/api/helpers/error_upsampling.py:L44-L74] — the “error-focused query” heuristic is a substring check on the raw query string:
_is_error_focused_querychecks only if"event.type:error"is present inrequest.GET['query']— This is brittle: equivalent SnQL queries (different spacing/casing/aliases, or usingevent.type = "error") won’t match, so upsampling may not apply when it should (or apply when the substring appears in an unrelated context). Fix: determine “error-focused” from the parsed Snuba/SnQL AST or from the normalized query representation used to build the dataset, not from the rawrequest.GET['query']string. If AST parsing isn’t available, at least normalize (lowercase, strip whitespace) and support common variants (e.g.,event.type:error,event.type = error,event.type:"error"). - [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py:L18-L33] Add a “fail closed” guard and a test: if
snuba_params.project_idsis empty/None, return False (no upsampling) rather than hashing an empty tuple and caching a potentially wrong eligibility. Then add a unit test for the “projects provided vs projects resolved” scenario to ensure the cache key matches the actual resolved project set. - [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py] Verify cache invalidation wiring: the helper caches for 60 seconds (
cache.set(cache_key, is_eligible, 60)) but the diff doesn’t showinvalidate_upsampling_cachebeing called on allowlist changes. Fix: ensure allowlist update code paths call invalidation, or reduce TTL and/or include an allowlist version in the cache key (e.g.,options.get(...).updated_ator a monotonically increasing config revision). - [ ] SUGGESTION — [src/sentry/api/endpoints/organization_events_stats.py] Add an end-to-end test that stores events and asserts the generated Snuba query uses
upsampled_countAND includes thesample_weighttransform. Current tests may only validate helper logic and not the full event normalization path that producessample_weight(see factory changes insrc/sentry/testutils/factories.py). - [ ] SUGGESTION — [src/sentry/api/endpoints/organization_events_stats.py] Run the full
tests/sentry/api/endpoints/test_organization_events_stats.pysuite and check for brittle assertions around error messages/response shapes. The diff changes the validation detail string ("If topEvents needs to be at least 1"->"topEvents needs to be at least 1"), which can break unrelated tests if they assert exact strings.
Suggestions
- [src/sentry/api/helpers/error_upsampling.py:L18-L33] Add a “fail closed” guard and a test: if
snuba_params.project_idsis empty/None, return False (no upsampling) rather than hashing an empty tuple and caching a potentially wrong eligibility. Then add a unit test for the “projects provided vs projects resolved” scenario to ensure the cache key matches the actual resolved project set. - [src/sentry/api/helpers/error_upsampling.py] Verify cache invalidation wiring: the helper caches for 60 seconds (
cache.set(cache_key, is_eligible, 60)) but the diff doesn’t showinvalidate_upsampling_cachebeing called on allowlist changes. Fix: ensure allowlist update code paths call invalidation, or reduce TTL and/or include an allowlist version in the cache key (e.g.,options.get(...).updated_ator a monotonically increasing config revision). - [src/sentry/api/endpoints/organization_events_stats.py] Add an end-to-end test that stores events and asserts the generated Snuba query uses
upsampled_countAND includes thesample_weighttransform. Current tests may only validate helper logic and not the full event normalization path that producessample_weight(see factory changes insrc/sentry/testutils/factories.py). - [src/sentry/api/endpoints/organization_events_stats.py] Run the full
tests/sentry/api/endpoints/test_organization_events_stats.pysuite and check for brittle assertions around error messages/response shapes. The diff changes the validation detail string ("If topEvents needs to be at least 1"->"topEvents needs to be at least 1"), which can break unrelated tests if they assert exact strings.
Posted by re-entry.ai · Risk governance for autonomous engineering teams
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
🔴 Risk Score: 65/100 · HIGH
| Dimension | Level |
|---|---|
| Likelihood | 🟠 High |
| Impact | 🟠 High |
| Detectability | 🟡 Medium |
Intent
Add an optimized “error upsampling” path for organization event stats by conditionally transforming Snuba query aggregations to account for sampling weights.
Summary
The PR introduces a new helper module that decides whether to apply error upsampling based on allowlisted projects and whether the request is querying error events, with a 60s cache to avoid repeated option lookups. It also adds a new SnQL aggregation function upsampled_count (implemented as sum(sample_weight)) and wires the endpoint to conditionally transform query columns. The biggest risks are correctness and cache safety: the cache key uses Python’s process-local hash() (non-deterministic across processes/restarts) and the eligibility logic assumes snuba_params.project_ids is always a concrete Sequence[int] with stable types. You must verify that sample_weight is guaranteed to exist whenever upsampled_count is selected, and that the cache key + invalidation logic cannot leave stale eligibility decisions across processes.
🎯 Review Focus
The cache key + eligibility normalization in src/sentry/api/helpers/error_upsampling.py (stable deterministic keying, correct handling of snuba_params.project_ids types, and ensuring upsampled_count is never selected without sample_weight).
✅ Action Checklist
- [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py:L18] Add a unit test that proves cache key stability across processes/restarts (or at least across multiple calls) by asserting the key derived from the same project IDs is identical. Concretely: expose the key helper (or test via
cache_keycomputation) and assert it matches a known sha256 for a fixed input. - [ ] SUGGESTION — [src/sentry/api/endpoints/organization_events_stats.py:L215] Verify
sample_weightexistence wheneverupsampled_countis selected. The new SnQL aggregation issum(sample_weight)(insrc/sentry/search/events/datasets/discover.py:1038-1060per risk report), but the endpoint only conditionally transforms columns. Add a guard intransform_query_columns_for_error_upsampling()to ensure the transformation is only applied when the query plan includessample_weight(or make the aggregation resilient, e.g.sum(coalesce(sample_weight, 1))if semantics allow). - [ ] SUGGESTION — [src/sentry/api/helpers/error_upsampling.py:L18] Don’t couple eligibility caching to request-specific query shape unless you can prove it’s invariant. Right now the cached eligibility result is combined with
_should_apply_sample_weight_transform(dataset, request)after a cache hit. Add a test for the same org/project set with two different request query formats to ensure the transformation decision changes correctly even when eligibility is cached.
Suggestions
- [src/sentry/api/helpers/error_upsampling.py:L18] Add a unit test that proves cache key stability across processes/restarts (or at least across multiple calls) by asserting the key derived from the same project IDs is identical. Concretely: expose the key helper (or test via
cache_keycomputation) and assert it matches a known sha256 for a fixed input. - [src/sentry/api/endpoints/organization_events_stats.py:L215] Verify
sample_weightexistence wheneverupsampled_countis selected. The new SnQL aggregation issum(sample_weight)(insrc/sentry/search/events/datasets/discover.py:1038-1060per risk report), but the endpoint only conditionally transforms columns. Add a guard intransform_query_columns_for_error_upsampling()to ensure the transformation is only applied when the query plan includessample_weight(or make the aggregation resilient, e.g.sum(coalesce(sample_weight, 1))if semantics allow). - [src/sentry/api/helpers/error_upsampling.py:L18] Don’t couple eligibility caching to request-specific query shape unless you can prove it’s invariant. Right now the cached eligibility result is combined with
_should_apply_sample_weight_transform(dataset, request)after a cache hit. Add a test for the same org/project set with two different request query formats to ensure the transformation decision changes correctly even when eligibility is cached.
📝 This review includes 4 inline comments (4 warnings)
Posted by re-entry.ai · Risk governance for autonomous engineering teams
| organization: Organization, | ||
| dataset: ModuleType, | ||
| request: Request, | ||
| ) -> bool: |
There was a problem hiding this comment.
Using Python's built-in hash() to derive the cache key makes the eligibility cache non-deterministic across interpreter restarts and processes. That means the same organization/project set can map to different keys, so cache invalidation in invalidate_upsampling_cache() may miss the entry that was originally written. The root cause is that the cache key is derived from a process-local hash instead of a stable digest of the project IDs.
| cached_result = cache.get(cache_key) | ||
| if cached_result is not None: | ||
| return cached_result and _should_apply_sample_weight_transform(dataset, request) | ||
|
|
There was a problem hiding this comment.
This helper assumes snuba_params.project_ids is always a concrete sequence of ints, but the call site passes through request-derived search params without validating that contract. If project_ids is None or contains non-comparable/non-int values, sorted(...) and the membership checks here will fail or produce incorrect eligibility decisions. The underlying issue is missing input normalization at the API boundary before caching and allowlist evaluation.
| configuration changes during request processing. This is intentional | ||
| to ensure we always have the latest configuration state. | ||
| """ | ||
| if not project_ids: |
There was a problem hiding this comment.
The allowlist is read from options and then compared directly with project_id values, but there is no normalization of either side. If the option is configured as strings or another sequence type, the membership test can silently return false for valid projects, disabling upsampling unexpectedly. This is a correctness issue caused by relying on implicit type matching for configuration data.
|
|
||
|
|
||
| def transform_query_columns_for_error_upsampling( | ||
| query_columns: Sequence[str], |
There was a problem hiding this comment.
invalidate_upsampling_cache() repeats the same unstable hash-based key derivation as the writer. Because Python's hash() is process-local, invalidation may target a different key than the one stored, leaving stale eligibility decisions in cache until TTL expiry. The root cause is the same non-stable cache key scheme used in both write and delete paths.
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🔴 Risk Score: 59/100 · HIGH
| Dimension | Level |
|---|---|
| Likelihood | 🟠 High |
| Impact | 🟠 High |
| Detectability | 🟡 Medium |
Intent
Add an optimized “error upsampling” path to organization event stats by transforming Snuba aggregations to use sample_weight when querying error events for fully allowlisted projects.
Summary
The change introduces a new helper (error_upsampling.py) that caches allowlist eligibility for 60s and conditionally rewrites Snuba query columns (replacing count() with upsampled_count() as count). It also adds a new Snuba dataset function upsampled_count implemented as sum(sample_weight), and wires the helper into the organization_events_stats endpoint. The highest risks are correctness: hard-coded aliasing can break downstream expectations, and the cache key uses Python’s non-stable hash() plus a cached value that is combined with query-dependent logic, which can yield inconsistent behavior across workers/processes. You should verify alias preservation, cache-key stability, and that sample_weight is guaranteed to exist whenever upsampled_count() is selected.
🎯 Review Focus
Verify correctness of the Snuba query rewrite end-to-end: alias preservation for the rewritten aggregation, stable/correct caching behavior across workers, and guaranteeing sample_weight exists whenever upsampled_count() is used.
✅ Action Checklist
- [ ] SUGGESTION — Fix cache key stability in
src/sentry/api/helpers/error_upsampling.py:L20by replacinghash(tuple(sorted(snuba_params.project_ids)))with a stable hash. Example:projects = ','.join(map(str, sorted(snuba_params.project_ids))); digest = hashlib.sha256(projects.encode()).hexdigest(); cache_key = f'error_upsampling_eligible:{organization.id}:{digest}'. - [ ] SUGGESTION — Make the cache decision consistent with the final transform decision in
src/sentry/api/helpers/error_upsampling.py:L26. Either (a) include the relevant query discriminator(s) (e.g., whether the request query targets error events / contains the same predicate used by_should_apply_sample_weight_transform) in the cache key, or (b) cache onlyis_eligibleand explicitly document/ensure the endpoint never treats the cached value as the final decision. - [ ] SUGGESTION — Preserve aggregation aliases when rewriting columns in
src/sentry/api/helpers/error_upsampling.py:L109. Implement alias extraction from the original column string (e.g., detectas <alias>), and emitupsampled_count() as <original_alias>; add a unit test covering a non-countalias to prevent regressions. - [ ] SUGGESTION — Add an integration assertion that
sample_weightis present/usable wheneverupsampled_count()is selected. Givensrc/sentry/search/events/datasets/discover.pydefinesupsampled_countassum(sample_weight), ensure the helper always injects the requiredsample_weighttransform when selectingupsampled_count(or makeupsampled_countrobust viasum(coalesce(sample_weight, 1))if that matches intended semantics). - [ ] SUGGESTION — Reduce test brittleness in
tests/sentry/api/helpers/test_error_upsampling.pyby asserting mock call arguments foroptions.get(not just return values). This ensures the helper is reading the expected option keys and defaults, aligning unit tests with endpoint behavior.
Suggestions
- Fix cache key stability in
src/sentry/api/helpers/error_upsampling.py:L20by replacinghash(tuple(sorted(snuba_params.project_ids)))with a stable hash. Example:projects = ','.join(map(str, sorted(snuba_params.project_ids))); digest = hashlib.sha256(projects.encode()).hexdigest(); cache_key = f'error_upsampling_eligible:{organization.id}:{digest}'. - Make the cache decision consistent with the final transform decision in
src/sentry/api/helpers/error_upsampling.py:L26. Either (a) include the relevant query discriminator(s) (e.g., whether the request query targets error events / contains the same predicate used by_should_apply_sample_weight_transform) in the cache key, or (b) cache onlyis_eligibleand explicitly document/ensure the endpoint never treats the cached value as the final decision. - Preserve aggregation aliases when rewriting columns in
src/sentry/api/helpers/error_upsampling.py:L109. Implement alias extraction from the original column string (e.g., detectas <alias>), and emitupsampled_count() as <original_alias>; add a unit test covering a non-countalias to prevent regressions. - Add an integration assertion that
sample_weightis present/usable wheneverupsampled_count()is selected. Givensrc/sentry/search/events/datasets/discover.pydefinesupsampled_countassum(sample_weight), ensure the helper always injects the requiredsample_weighttransform when selectingupsampled_count(or makeupsampled_countrobust viasum(coalesce(sample_weight, 1))if that matches intended semantics). - Reduce test brittleness in
tests/sentry/api/helpers/test_error_upsampling.pyby asserting mock call arguments foroptions.get(not just return values). This ensures the helper is reading the expected option keys and defaults, aligning unit tests with endpoint behavior.
📝 This review includes 4 inline comments (1 critical, 3 warnings)
Posted by re-entry.ai · Risk governance for autonomous engineering teams
| and query context. Only apply for error events since sample_weight doesn't exist | ||
| for transactions. | ||
| """ | ||
| from sentry.snuba import discover, errors |
There was a problem hiding this comment.
🚨 CRITICAL
Quote: transformed_columns.append("upsampled_count() as count")
Issue: The alias is hard-coded to count. If the original query expected a different alias (e.g., count() as total), this will break downstream consumers expecting total. This is a correctness/data integrity risk for query results.
Fix: Preserve the original alias when present. For example, detect as <alias> in column and reuse it:
# pseudo
alias = extract_alias(column)
transformed_columns.append(f"upsampled_count() as {alias or 'count'}")
``` (see also L109)
---
_[re-entry.ai](https://re-entry.ai)_| @@ -0,0 +1,101 @@ | |||
| from unittest.mock import Mock, patch | |||
There was a problem hiding this comment.
Quote: from unittest.mock import Mock, patch
Issue: The tests patch options in only one helper test, but other helpers depend on request/query parsing and Snuba dataset selection. Over-mocking can make tests pass even if integration behavior changes (e.g., if options.get is called with different keys/arguments).
Fix: Assert mock call arguments to ensure the helper is using the expected option key(s), e.g.:
mock_options.get.assert_called_with(<expected_key>, <expected_default>)and/or add integration-style tests that exercise the helpers without patching where feasible. (same pattern in tests/snuba/api/endpoints/test_organization_events_stats.py:L3567, tests/snuba/api/endpoints/test_organization_events_stats.py:L3638)
| request: Request, | ||
| ) -> bool: | ||
| """ | ||
| Determine if this query should use error upsampling transformations. |
There was a problem hiding this comment.
Quote: cache_key = f"error_upsampling_eligible:{organization.id}:{hash(tuple(sorted(snuba_params.project_ids)))}"
Issue: Using Python's built-in hash() for a cache key is not stable across process restarts (and can vary by hash randomization). This can lead to cache misses after deploys and, more importantly, potential key collisions within a process if the hash space is stressed.
Fix: Use a stable hash (e.g., sha256) or avoid hashing entirely:
import hashlib
...
projects = ",".join(map(str, sorted(snuba_params.project_ids)))
cache_key = f"error_upsampling_eligible:{organization.id}:{hashlib.sha256(projects.encode()).hexdigest()}"| Performance optimization: Cache allowlist eligibility for 60 seconds to avoid | ||
| expensive repeated option lookups during high-traffic periods. This is safe | ||
| because allowlist changes are infrequent and eventual consistency is acceptable. | ||
| """ |
There was a problem hiding this comment.
Quote: if cached_result is not None: return cached_result and _should_apply_sample_weight_transform(dataset, request)
Issue: The cached value only represents allowlist eligibility, but the return condition also depends on _should_apply_sample_weight_transform(dataset, request) which inspects request.GET['query']. This means the cache can cause different behavior for different queries within the same 60s window (not necessarily wrong), but it also means the function's output is not purely a function of the cached inputs. If callers assume the cached eligibility implies a stable transformation decision, this can cause semantic inconsistencies.
Fix: Either (a) cache only the eligibility and keep the final decision fully computed (current behavior), but document/ensure callers don't treat the cached result as final; or (b) include the relevant query discriminator(s) in the cache key (e.g., whether the query contains event.type:error).
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🚨 Risk Score: 92/100 · CRITICAL
| Dimension | Level |
|---|---|
| Likelihood | 🔴 Critical |
| Impact | 🔴 Critical |
| Detectability | 🟠 High |
Intent
Add a cached, allowlist-gated “error upsampling” path that rewrites Snuba aggregations so organization event stats can report sample-weighted error counts.
Summary
Behaviorally, the PR introduces a new helper that decides whether to apply error upsampling based on project allowlisting and whether the request query appears to target error events, then rewrites Snuba query columns by replacing count() with a new upsampled_count() (implemented as sum(sample_weight)). The biggest risks are (1) correctness/contract mismatch: upsampled_count assumes sample_weight exists for the events being aggregated, but the transformation eligibility is request- and dataset-dependent and is cached in a way that can short-circuit needed predicates; and (2) test/fixture masking: test utilities swallow exceptions while setting sample_rate, and endpoint tests inject contexts.error_sampling.client_sample_rate directly, potentially bypassing the real ingestion/normalization contract. Before merge, verify that sample_weight is guaranteed whenever upsampled_count is selected, and that the cache key/predicate logic cannot apply the transformation for the wrong query shape or dataset.
🎯 Review Focus
Verify the end-to-end contract: whenever the query rewrite selects upsampled_count() (i.e., count() is replaced), sample_weight must be present for the aggregated events, and the cached eligibility logic must not apply the rewrite for a different request/query shape than intended.
✅ Action Checklist
- SUGGESTION — Fix the cache key to be stable and collision-resistant. In
src/sentry/api/helpers/error_upsampling.py, replacehash(tuple(sorted(snuba_params.project_ids)))with a deterministic string, e.g.:
project_part = ",".join(map(str, sorted(snuba_params.project_ids)))
cache_key = f"error_upsampling_eligible:{organization.id}:{project_part}"(or use hashlib.sha256(project_part.encode()).hexdigest()).
- SUGGESTION — Refactor the cache usage so request/dataset predicates are never skipped. In
src/sentry/api/helpers/error_upsampling.py, change the cache hit path to:
cached_is_eligible = cache.get(cache_key)
if cached_is_eligible is None:
cached_is_eligible = _are_all_projects_error_upsampled(...)
cache.set(cache_key, cached_is_eligible, 60)
return bool(cached_is_eligible) and _should_apply_sample_weight_transform(dataset, request)This ensures _should_apply_sample_weight_transform(dataset, request) is evaluated for every request.
- SUGGESTION — Make
upsampled_countnull-safe or enforce the contract. Givenupsampled_countissum(sample_weight)(seesrc/sentry/search/events/datasets/discover.py:1038-1060per your evidence), ensure either (a) the transformation that materializessample_weightis always applied wheneverupsampled_countis selected, or (b) update the aggregation to tolerate missing weights, e.g.sum(coalesce(sample_weight, 0))(or SnQL equivalent). Add an assertion/integration test that fails ifsample_weightis absent butupsampled_countis used. - SUGGESTION — Tighten the test utility exception handling and validate bounds. In
src/sentry/testutils/factories.py, replace broadexcept Exception: passwith targeted exceptions and optionally enforce domain constraints (e.g.,0 < sample_rate <= 1) if that’s the intended meaning ofclient_sample_rate. - SUGGESTION — Align endpoint tests with the real ingestion/normalization contract. In
tests/snuba/api/endpoints/test_organization_events_stats.py, avoid injectingcontexts.error_sampling.client_sample_ratein a way that bypasses the factory’s normalization path. Either rely solely onstore_event+ the factory mapping, or explicitly assert that the normalized field used by Snuba (sample_rate/sample_weight) is present after ingestion.
Suggestions
- Fix the cache key to be stable and collision-resistant. In
src/sentry/api/helpers/error_upsampling.py, replacehash(tuple(sorted(snuba_params.project_ids)))with a deterministic string, e.g.:
project_part = ",".join(map(str, sorted(snuba_params.project_ids)))
cache_key = f"error_upsampling_eligible:{organization.id}:{project_part}"(or use hashlib.sha256(project_part.encode()).hexdigest()).
- Refactor the cache usage so request/dataset predicates are never skipped. In
src/sentry/api/helpers/error_upsampling.py, change the cache hit path to:
cached_is_eligible = cache.get(cache_key)
if cached_is_eligible is None:
cached_is_eligible = _are_all_projects_error_upsampled(...)
cache.set(cache_key, cached_is_eligible, 60)
return bool(cached_is_eligible) and _should_apply_sample_weight_transform(dataset, request)This ensures _should_apply_sample_weight_transform(dataset, request) is evaluated for every request.
- Make
upsampled_countnull-safe or enforce the contract. Givenupsampled_countissum(sample_weight)(seesrc/sentry/search/events/datasets/discover.py:1038-1060per your evidence), ensure either (a) the transformation that materializessample_weightis always applied wheneverupsampled_countis selected, or (b) update the aggregation to tolerate missing weights, e.g.sum(coalesce(sample_weight, 0))(or SnQL equivalent). Add an assertion/integration test that fails ifsample_weightis absent butupsampled_countis used. - Tighten the test utility exception handling and validate bounds. In
src/sentry/testutils/factories.py, replace broadexcept Exception: passwith targeted exceptions and optionally enforce domain constraints (e.g.,0 < sample_rate <= 1) if that’s the intended meaning ofclient_sample_rate. - Align endpoint tests with the real ingestion/normalization contract. In
tests/snuba/api/endpoints/test_organization_events_stats.py, avoid injectingcontexts.error_sampling.client_sample_ratein a way that bypasses the factory’s normalization path. Either rely solely onstore_event+ the factory mapping, or explicitly assert that the normalized field used by Snuba (sample_rate/sample_weight) is present after ingestion.
📝 This review includes 3 inline comments (3 warnings)
Posted by re-entry.ai · Risk governance for autonomous engineering teams
| request: Request, | ||
| ) -> bool: | ||
| """ | ||
| Determine if this query should use error upsampling transformations. |
There was a problem hiding this comment.
Issue: Cache key uses hash(tuple(sorted(snuba_params.project_ids))). Python's hash() is randomized per process (and can vary across restarts), so cached entries may be effectively non-deterministic and can also collide more than expected across different inputs.
Fix: Use a stable hash (e.g., join IDs) instead of hash(), for example:
project_part = ",".join(map(str, sorted(snuba_params.project_ids)))
cache_key = f"error_upsampling_eligible:{organization.id}:{project_part}"(or use hashlib like sha256 over the joined string). (see also L24)
| cached_result = cache.get(cache_key) | ||
| if cached_result is not None: | ||
| return cached_result and _should_apply_sample_weight_transform(dataset, request) | ||
|
|
There was a problem hiding this comment.
Issue: Cache stores only is_eligible for 60 seconds, but the final decision also depends on _should_apply_sample_weight_transform(dataset, request). If the request context changes (e.g., different query parameter) within the 60s window, the cached eligibility may be correct but the overall decision still needs to be recomputed. The current implementation recomputes _should_apply_sample_weight_transform only on cache hit when cached_result is truthy; see the short-circuiting behavior.
Fix: Ensure the context predicate is always evaluated on cache hits (see previous finding), and consider caching only the allowlist eligibility while always applying the dataset/request predicate independently.
| pass | ||
| if client_sample_rate: | ||
| try: | ||
| normalized_data["sample_rate"] = float(client_sample_rate) |
There was a problem hiding this comment.
Issue: The helper swallows all exceptions when reading normalized_data and when converting client_sample_rate to float, which can hide data-shaping bugs and make tests pass while production behavior differs.
Evidence:
- except Exception:
-
pass
...
-
except Exception: -
pass
Fix: Catch only expected exceptions (e.g., TypeError, ValueError) and/or add a debug assertion in test utilities. For example:
try:
client_sample_rate = (
normalized_data.get("contexts", {})
.get("error_sampling", {})
.get("client_sample_rate")
)
except (AttributeError, TypeError):
return
if client_sample_rate is not None:
try:
normalized_data["sample_rate"] = float(client_sample_rate)
except (TypeError, ValueError):
returnAlso consider validating bounds (e.g., 0 < sample_rate <= 1) if that’s the intended domain.
Martian Code Review Benchmark PR (mirrored from source #3)