feat(upsampling) - Support upsampled error count with performance optimizations - #1
feat(upsampling) - Support upsampled error count with performance optimizations#1linxia0415 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.
AI代码审查报告
变更概览
本次 PR 涉及 8 个文件,新增 +480 行,删除 -6 行。
功能变更摘要
本 PR 新增错误上采样功能,用于在查询事件统计时自动补充低采样率的错误数据。主要实现包括核心上采样逻辑、API 端点集成、搜索数据集扩展及完整的单元测试和集成测试覆盖。
文件变更摘要
| 文件 | 变更 | 行数 | 摘要 | 发现问题 |
|---|---|---|---|---|
src/sentry/api/helpers/error_upsampling.py |
新增 | +140/-0 | 实现错误上采样的核心逻辑 | 3 个 |
src/sentry/api/endpoints/organization_events_stats.py |
修改 | +33/-5 | 集成错误上采样到组织事件统计 API 端点 | 1 个 |
tests/snuba/api/endpoints/test_organization_events_stats.py |
修改 | +171/-0 | 事件统计 API 的集成测试用例 | — |
tests/sentry/api/helpers/test_error_upsampling.py |
新增 | +101/-0 | 错误上采样功能的单元测试 | — |
src/sentry/testutils/factories.py |
修改 | +20/-1 | 增加测试数据工厂方法以支持上采样测试 | — |
src/sentry/search/events/datasets/discover.py |
修改 | +12/-0 | 扩展搜索数据集以支持上采样查询 | — |
pyproject.toml |
修改 | +2/-0 | 添加新依赖或配置项以支持错误上采样功能 | — |
sentry-repo |
新增 | +1/-0 | 新增项目配置文件 | — |
问题严重级别分布
| 级别 | 数量 | 占比 |
|---|---|---|
| 🟡 中危 | 3 | 75% |
| 🟢 低危 | 1 | 25% |
代表性问题(至多 10 条,按严重级别优先)
- 🟡 中危
src/sentry/api/endpoints/organization_events_stats.pyL232: 未验证的函数返回值赋值
line 232 处调用 transform_query_columns_for_error_upsampling() 并将返回值赋给 final_columns,但未验证返…
2. 🟡 中危 src/sentry/api/helpers/error_upsampling.py L26: 缓存键哈希不稳定
line 26 处使用 Python 内置 hash() 函数生成缓存键,但 hash() 默认启用哈希随机化(PYTHONHASHSEED=random),导致相同输入在…
3. 🟡 中危 src/sentry/api/helpers/error_upsampling.py L131: 字符串子串匹配导致误判
line 131 处使用 "event.type:error" in query 进行简单子串匹配,会错误匹配到 "event.type:error_transacti… 4. 🟢 **低危** src/sentry/api/helpers/error_upsampling.py` L104: 函数内延迟导入
line 104 和 line 110 在函数内部进行模块导入。虽然这可能是为了避免循环导入,但延迟导入会隐藏依赖关系,增加代码维护难度,且每次函数调用都增加导入开销(尽管 Pyth…
未发帖的低优先级项
当前策略为 balanced:以下 1 条未发成行评,以免掩盖安全与正确性问题;若团队关注代码风格与微优化,可在此浏览或改为 full。
- 低危
src/sentry/api/helpers/error_upsampling.pyL104:函数内延迟导入 line 104 和 line 110 在函数内部进行模块导入。虽然这可能是为了避免循环导入,但延迟导入会隐藏依赖关系,增加代码维护难度,且每次函数调用都增加导入开销(尽管 Python 会缓存已导入模块)。 如果确实存…
行评发帖:共检出 4 处,本次策略
balanced下发 3 处行评;其余见上表或未发帖列表。
由 CodeHawk 提供支持 · nuwa
| if top_events > 0: | ||
| # Apply upsampling transformation just before query execution | ||
| # This late transformation ensures we use the most current schema assumptions | ||
| if upsampling_enabled: |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
未验证的函数返回值赋值
line 232 处调用 transform_query_columns_for_error_upsampling() 并将返回值赋给 final_columns,但未验证返回值是否为 None。该函数在 line 276 和 line 295 处也被同样方式调用。如果 transform_query_columns_for_error_upsampling() 在任何路径下返回 None(如输入验证失败、异常处理不当或配置错误),final_columns 将被设为 None,导致后续 line 235-251、line 278-291 或 line 297-323 中的查询执行代码在使用 final_columns 时触发 AttributeError 或 TypeError,使请求失败。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 232
💬 详细说明:
- 未验证的函数返回值赋值
- line 232 处调用 transform_query_columns_for_error_upsampling() 并将返回值赋给 final_columns,但未验证返回值是否为 None。该函数在 line 276 和 line 295 处也被同样方式调用。如果 transform_query_columns_for_error_upsampling() 在任何路径下返回 None(如输入验证失败、异常处理不当或配置错误),final_columns 将被设为 None,导致后续 line 235-251、line 278-291 或 line 297-323 中的查询执行代码在使用 final_columns 时触发 AttributeError 或 TypeError,使请求失败。
📝 问题代码:
final_columns = transform_query_columns_for_error_upsampling(query_columns)
💡 修复建议:
使用短路求值保护赋值操作,确保 transform_query_columns_for_error_upsampling() 返回 None 时回退到原始 query_columns。需要在三处调用点(line 232、276、295)统一应用此修复。
✅ 修复示例:
if upsampling_enabled:
final_columns = transform_query_columns_for_error_upsampling(query_columns) or query_columns
🔗 参考链接
无
| 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.
🟡 AI 代码审查发现问题
📋 问题概述
缓存键哈希不稳定
line 26 处使用 Python 内置 hash() 函数生成缓存键,但 hash() 默认启用哈希随机化(PYTHONHASHSEED=random),导致相同输入在不同进程或应用重启后产生不同的哈希值。在分布式部署或多 worker 环境中,这会造成缓存命中率骤降——一个进程设置的缓存,其他进程无法命中。
line 70 的 invalidate_upsampling_cache 函数使用相同的哈希逻辑,同样受此问题影响。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 26
💬 详细说明:
- 缓存键哈希不稳定
- line 26 处使用 Python 内置
hash()函数生成缓存键,但hash()默认启用哈希随机化(PYTHONHASHSEED=random),导致相同输入在不同进程或应用重启后产生不同的哈希值。在分布式部署或多 worker 环境中,这会造成缓存命中率骤降——一个进程设置的缓存,其他进程无法命中。 - line 70 的
invalidate_upsampling_cache函数使用相同的哈希逻辑,同样受此问题影响。
📝 问题代码:
cache_key = f"error_upsampling_eligible:{organization.id}:{hash(tuple(sorted(snuba_params.project_ids)))}"
💡 修复建议:
使用确定性哈希算法替代
hash()。推荐使用hashlib.md5或hashlib.sha256对排序后的项目 ID 元组进行哈希,确保跨进程缓存键一致。
操作步骤:
- 在文件顶部导入
hashlib - 将
hash(tuple(sorted(...)))替换为hashlib.md5(str(tuple(sorted(...))).encode()).hexdigest() - 同步修改 line 70 的
invalidate_upsampling_cache函数使用相同的哈希逻辑
✅ 修复示例:
import hashlib
def is_errors_query_for_error_upsampled_projects(
snuba_params: SnubaParams,
organization: Organization,
dataset: ModuleType,
request: Request,
) -> bool:
"""
Determine if this query should use error upsampling transformations.
Only applies when ALL projects are allowlisted and we're querying error events.
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.
"""
project_ids_hash = hashlib.md5(
str(tuple(sorted(snuba_params.project_ids))).encode()
).hexdigest()
cache_key = f"error_upsampling_eligible:{organization.id}:{project_ids_hash}"
# Check cache first for performance optimization
cached_result = cache.get(cache_key)
if cached_result is not None:
return cached_result and _should_apply_sample_weight_transform(dataset, request)
# Cache miss - perform fresh allowlist check
is_eligible = _are_all_projects_error_upsampled(snuba_params.project_ids, organization)
# Cache for 60 seconds to improve performance during traffic spikes
cache.set(cache_key, is_eligible, 60)
return is_eligible and _should_apply_sample_weight_transform(dataset, request)
🔗 参考链接
无
|
|
||
|
|
||
| def _is_error_focused_query(request: Request) -> bool: | ||
| """ |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
字符串子串匹配导致误判
line 131 处使用 "event.type:error" in query 进行简单子串匹配,会错误匹配到 "event.type:error_transaction"、"event.type:error_sample" 等非 error 类型的查询条件。虽然注释说明"err on the side of caution",但过度宽泛的匹配会导致不必要的 upsampled_count() 转换,可能产生错误的聚合结果或性能开销。
触发条件:当查询字符串包含 event.type:error 作为子串但并非精确匹配时(如 event.type:error_rate),函数会误判为 error 查询。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 131
💬 详细说明:
- 字符串子串匹配导致误判
- line 131 处使用
"event.type:error" in query进行简单子串匹配,会错误匹配到"event.type:error_transaction"、"event.type:error_sample"等非 error 类型的查询条件。虽然注释说明"err on the side of caution",但过度宽泛的匹配会导致不必要的upsampled_count()转换,可能产生错误的聚合结果或性能开销。 - 触发条件:当查询字符串包含
event.type:error作为子串但并非精确匹配时(如event.type:error_rate),函数会误判为 error 查询。
📝 问题代码:
if "event.type:error" in query:
💡 修复建议:
使用更精确的模式匹配来识别
event.type:error。建议采用正则表达式或词法分析,确保匹配的是完整的 token 而非子串。
操作步骤:
- 导入
re模块 - 使用正则表达式
r'event\.type\s*:\s*error\b'匹配完整的event.type:error,其中\b确保单词边界 - 考虑处理引号包裹的情况如
event.type:"error"
✅ 修复示例:
import re
def _is_error_focused_query(request: Request) -> bool:
"""
Check if a query is focused on error events.
Reduced to only check for event.type:error to err on the side of caution.
"""
query = request.GET.get("query", "").lower()
# Match event.type:error as a complete token, not substring
# Handles: event.type:error, event.type: error, event.type:"error"
if re.search(r'event\.type\s*[=:]\s*["\']?error["\']?\b', query):
return True
return False
🔗 参考链接
无
Test 3