Skip to content

Commit 258f072

Browse files
committed
feat(preprod): Add Size Analysis detector
1 parent b4b9c26 commit 258f072

File tree

3 files changed

+622
-0
lines changed

3 files changed

+622
-0
lines changed

src/sentry/preprod/grouptype.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
from dataclasses import dataclass
44

5+
import sentry.preprod.size_analysis.grouptype # noqa: F401,F403
56
from sentry.issues.grouptype import GroupCategory, GroupType
67
from sentry.types.group import PriorityLevel
78

9+
# We have to import sentry.preprod.size_analysis.grouptype above.
10+
# grouptype modules in root packages (src/sentry/*) are auto imported
11+
# but more deeply nested ones are not.
12+
813

914
@dataclass(frozen=True)
1015
class PreprodStaticGroupType(GroupType):
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from dataclasses import dataclass
5+
from datetime import datetime
6+
from datetime import timezone as dt_timezone
7+
from typing import Any, NotRequired, TypeAlias, TypedDict
8+
from uuid import uuid4
9+
10+
from sentry.issues.grouptype import GroupCategory, GroupType
11+
from sentry.issues.issue_occurrence import IssueEvidence
12+
from sentry.types.group import PriorityLevel
13+
from sentry.utils import metrics
14+
from sentry.workflow_engine.endpoints.validators.base import BaseDetectorTypeValidator
15+
from sentry.workflow_engine.handlers.detector.base import (
16+
BaseDetectorHandler,
17+
DetectorOccurrence,
18+
GroupedDetectorEvaluationResult,
19+
)
20+
from sentry.workflow_engine.models import DataPacket
21+
from sentry.workflow_engine.processors.data_condition_group import (
22+
ProcessedDataConditionGroup,
23+
process_data_condition_group,
24+
)
25+
from sentry.workflow_engine.types import (
26+
DetectorEvaluationResult,
27+
DetectorPriorityLevel,
28+
DetectorSettings,
29+
)
30+
31+
logger = logging.getLogger(__name__)
32+
33+
34+
class SizeAnalysisValue(TypedDict):
35+
head_install_size_bytes: int
36+
head_download_size_bytes: int
37+
base_install_size_bytes: NotRequired[int | None]
38+
base_download_size_bytes: NotRequired[int | None]
39+
40+
41+
SizeAnalysisDataPacket = DataPacket[SizeAnalysisValue]
42+
43+
# The value extracted from a data packet and evaluated against conditions.
44+
# int for absolute values (bytes), float for relative diffs (ratios).
45+
SizeAnalysisEvaluation: TypeAlias = int | float
46+
47+
48+
class PreprodSizeAnalysisDetectorHandler(
49+
BaseDetectorHandler[SizeAnalysisValue, SizeAnalysisEvaluation]
50+
):
51+
def evaluate_impl(self, data_packet: SizeAnalysisDataPacket) -> GroupedDetectorEvaluationResult:
52+
value = self.extract_value(data_packet)
53+
evaluation, priority = self._evaluate_conditions(value)
54+
if evaluation is None or priority is None:
55+
return GroupedDetectorEvaluationResult(result={}, tainted=False)
56+
57+
detector_occurrence, event_data = self.create_occurrence(evaluation, data_packet, priority)
58+
occurrence = detector_occurrence.to_issue_occurrence(
59+
occurrence_id=event_data["event_id"],
60+
project_id=self.detector.project_id,
61+
status=priority,
62+
additional_evidence_data={},
63+
fingerprint=[uuid4().hex],
64+
)
65+
result = DetectorEvaluationResult(
66+
group_key=None,
67+
is_triggered=True,
68+
priority=priority,
69+
event_data=event_data,
70+
result=occurrence,
71+
)
72+
return GroupedDetectorEvaluationResult(result={None: result}, tainted=False)
73+
74+
def _evaluate_conditions(
75+
self, value: SizeAnalysisEvaluation
76+
) -> tuple[ProcessedDataConditionGroup | None, DetectorPriorityLevel | None]:
77+
if not self.condition_group:
78+
metrics.incr("workflow_engine.detector.skipping_invalid_condition_group")
79+
return None, None
80+
81+
condition_evaluation, _ = process_data_condition_group(self.condition_group, value)
82+
if not condition_evaluation.logic_result.triggered:
83+
return None, None
84+
85+
priorities = [
86+
condition_result.result
87+
for condition_result in condition_evaluation.condition_results
88+
if isinstance(condition_result.result, DetectorPriorityLevel)
89+
]
90+
if not priorities:
91+
return None, None
92+
93+
return condition_evaluation, max(priorities)
94+
95+
def _extract_head(self, data_packet: SizeAnalysisDataPacket) -> int:
96+
measurement = self.detector.config["measurement"]
97+
match measurement:
98+
case "install_size":
99+
return data_packet.packet["head_install_size_bytes"]
100+
case "download_size":
101+
return data_packet.packet["head_download_size_bytes"]
102+
case _:
103+
raise ValueError(f"Unknown measurement: {measurement}")
104+
105+
def _extract_base(self, data_packet: SizeAnalysisDataPacket) -> int:
106+
measurement = self.detector.config["measurement"]
107+
match measurement:
108+
case "install_size":
109+
base = data_packet.packet.get("base_install_size_bytes")
110+
case "download_size":
111+
base = data_packet.packet.get("base_download_size_bytes")
112+
case _:
113+
raise ValueError(f"Unknown measurement: {measurement}")
114+
if base is None:
115+
raise ValueError(f"Missing base value for measurement: {measurement}")
116+
return base
117+
118+
def extract_value(self, data_packet: SizeAnalysisDataPacket) -> SizeAnalysisEvaluation:
119+
threshold_type = self.detector.config["threshold_type"]
120+
match threshold_type:
121+
case "absolute_threshold":
122+
return self._extract_head(data_packet)
123+
case "absolute_diff":
124+
return self._extract_head(data_packet) - self._extract_base(data_packet)
125+
case "relative_diff":
126+
base = self._extract_base(data_packet)
127+
return (self._extract_head(data_packet) - base) / base
128+
case _:
129+
raise ValueError(f"Unknown threshold_type: {threshold_type}")
130+
131+
def create_occurrence(
132+
self,
133+
evaluation_result: ProcessedDataConditionGroup,
134+
data_packet: SizeAnalysisDataPacket,
135+
priority: DetectorPriorityLevel,
136+
) -> tuple[DetectorOccurrence, dict[str, Any]]:
137+
current_timestamp = datetime.now(dt_timezone.utc)
138+
139+
occurrence = DetectorOccurrence(
140+
issue_title="size",
141+
subtitle="A preprod static analysis issue was detected",
142+
evidence_display=[
143+
IssueEvidence(
144+
name="Source",
145+
value=data_packet.source_id,
146+
important=True,
147+
)
148+
],
149+
type=PreprodSizeAnalysisGroupType,
150+
level="warning",
151+
culprit="",
152+
priority=priority,
153+
)
154+
155+
event_data = {
156+
"event_id": uuid4().hex,
157+
"project_id": self.detector.project_id,
158+
"platform": "android",
159+
"received": current_timestamp.timestamp(),
160+
"timestamp": current_timestamp.timestamp(),
161+
"tags": {},
162+
}
163+
164+
return occurrence, event_data
165+
166+
def extract_dedupe_value(self, data_packet: SizeAnalysisDataPacket) -> int:
167+
raise NotImplementedError
168+
169+
170+
class PreprodSizeAnalysisDetectorValidator(BaseDetectorTypeValidator):
171+
pass
172+
173+
174+
@dataclass(frozen=True)
175+
class PreprodSizeAnalysisGroupType(GroupType):
176+
type_id = 11003
177+
slug = "preprod_size_analysis"
178+
description = "Size Analysis"
179+
category = GroupCategory.PREPROD.value
180+
category_v2 = GroupCategory.PREPROD.value
181+
default_priority = PriorityLevel.LOW
182+
released = False
183+
enable_auto_resolve = True
184+
enable_escalation_detection = False
185+
detector_settings = DetectorSettings(
186+
handler=PreprodSizeAnalysisDetectorHandler,
187+
validator=PreprodSizeAnalysisDetectorValidator,
188+
config_schema={
189+
"$schema": "https://json-schema.org/draft/2020-12/schema",
190+
"description": "Configuration for preprod static analysis detector",
191+
"type": "object",
192+
"properties": {
193+
"threshold_type": {
194+
"type": "string",
195+
"enum": ["absolute_diff", "absolute_threshold", "relative_diff"],
196+
"description": "The type of threshold to apply",
197+
},
198+
"measurement": {
199+
"type": "string",
200+
"enum": ["install_size", "download_size"],
201+
"description": "The measurement to track",
202+
},
203+
},
204+
"required": ["threshold_type", "measurement"],
205+
"additionalProperties": False,
206+
},
207+
)

0 commit comments

Comments
 (0)