Skip to content

Commit 53b34dd

Browse files
feat(perf-issues): initialize giant http payload detector (#47264)
Initializes the giant http payload detector. This is a simple detector that just compares the payload size to a threshold (10mb). We won't be able to detect issues from this as the SDK changes to get the payload size for fetch and xhr is not in place, but putting this in place to have it ready. --------- Co-authored-by: George Gritsouk <[email protected]>
1 parent 0e77982 commit 53b34dd

File tree

8 files changed

+193
-0
lines changed

8 files changed

+193
-0
lines changed

src/sentry/conf/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,8 @@ def SOCIAL_AUTH_DEFAULT_USERNAME():
12721272
"organizations:performance-consecutive-db-issue": False,
12731273
# Enable consecutive http performance issue type
12741274
"organizations:performance-consecutive-http-detector": False,
1275+
# Enable consecutive http performance issue type
1276+
"organizations:performance-large-http-payload-detector": False,
12751277
# Enable slow DB performance issue type
12761278
"organizations:performance-slow-db-issue": False,
12771279
# Enable N+1 API Calls performance issue type

src/sentry/issues/grouptype.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ class PerformanceDBMainThreadGroupType(PerformanceGroupTypeDefaults, GroupType):
284284
category = GroupCategory.PERFORMANCE.value
285285

286286

287+
@dataclass(frozen=True)
288+
class PerformanceLargeHTTPPayloadGroupType(PerformanceGroupTypeDefaults, GroupType):
289+
type_id = 1015
290+
slug = "performance_large_http_payload"
291+
description = "Large HTTP payload"
292+
category = GroupCategory.PERFORMANCE.value
293+
294+
287295
@dataclass(frozen=True)
288296
class ProfileFileIOGroupType(GroupType):
289297
type_id = 2001

src/sentry/utils/performance_issues/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
PerformanceConsecutiveHTTPQueriesGroupType,
1717
PerformanceDBMainThreadGroupType,
1818
PerformanceFileIOMainThreadGroupType,
19+
PerformanceLargeHTTPPayloadGroupType,
1920
PerformanceMNPlusOneDBQueriesGroupType,
2021
PerformanceNPlusOneAPICallsGroupType,
2122
PerformanceNPlusOneGroupType,
@@ -36,6 +37,7 @@ class DetectorType(Enum):
3637
N_PLUS_ONE_API_CALLS = "n_plus_one_api_calls"
3738
CONSECUTIVE_DB_OP = "consecutive_db"
3839
CONSECUTIVE_HTTP_OP = "consecutive_http"
40+
LARGE_HTTP_PAYLOAD = "large_http_payload"
3941
FILE_IO_MAIN_THREAD = "file_io_main_thread"
4042
M_N_PLUS_ONE_DB = "m_n_plus_one_db"
4143
UNCOMPRESSED_ASSETS = "uncompressed_assets"
@@ -54,6 +56,7 @@ class DetectorType(Enum):
5456
DetectorType.UNCOMPRESSED_ASSETS: PerformanceUncompressedAssetsGroupType,
5557
DetectorType.CONSECUTIVE_HTTP_OP: PerformanceConsecutiveHTTPQueriesGroupType,
5658
DetectorType.DB_MAIN_THREAD: PerformanceDBMainThreadGroupType,
59+
DetectorType.LARGE_HTTP_PAYLOAD: PerformanceLargeHTTPPayloadGroupType,
5760
}
5861

5962

@@ -63,6 +66,7 @@ class DetectorType(Enum):
6366
DetectorType.N_PLUS_ONE_DB_QUERIES_EXTENDED: "performance.issues.n_plus_one_db_ext.problem-creation",
6467
DetectorType.CONSECUTIVE_DB_OP: "performance.issues.consecutive_db.problem-creation",
6568
DetectorType.CONSECUTIVE_HTTP_OP: "performance.issues.consecutive_http.flag_disabled",
69+
DetectorType.LARGE_HTTP_PAYLOAD: "performance.issues.large_http_payload.flag_disabled",
6670
DetectorType.N_PLUS_ONE_API_CALLS: "performance.issues.n_plus_one_api_calls.problem-creation",
6771
DetectorType.FILE_IO_MAIN_THREAD: "performance.issues.file_io_main_thread.problem-creation",
6872
DetectorType.UNCOMPRESSED_ASSETS: "performance.issues.compressed_assets.problem-creation",

src/sentry/utils/performance_issues/detectors/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .consecutive_db_detector import ConsecutiveDBSpanDetector # NOQA
22
from .consecutive_http_detector import ConsecutiveHTTPSpanDetector # NOQA
33
from .io_main_thread_detector import DBMainThreadDetector, FileIOMainThreadDetector # NOQA
4+
from .large_payload_detector import LargeHTTPPayloadDetector # NOQA
45
from .mn_plus_one_db_span_detector import MNPlusOneDBSpanDetector # NOQA
56
from .n_plus_one_api_calls_detector import NPlusOneAPICallsDetector # NOQA
67
from .n_plus_one_db_span_detector import ( # NOQA
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
from sentry import features
4+
from sentry.issues.grouptype import PerformanceLargeHTTPPayloadGroupType
5+
from sentry.models import Organization, Project
6+
7+
from ..base import DetectorType, PerformanceDetector, fingerprint_http_spans
8+
from ..performance_problem import PerformanceProblem
9+
from ..types import Span
10+
11+
12+
class LargeHTTPPayloadDetector(PerformanceDetector):
13+
__slots__ = "stored_problems"
14+
15+
type: DetectorType = DetectorType.LARGE_HTTP_PAYLOAD
16+
settings_key = DetectorType.LARGE_HTTP_PAYLOAD
17+
18+
def init(self):
19+
self.stored_problems: dict[str, PerformanceProblem] = {}
20+
self.consecutive_http_spans: list[Span] = []
21+
22+
def visit_span(self, span: Span) -> None:
23+
if not LargeHTTPPayloadDetector._is_span_eligible(span):
24+
return
25+
26+
data = span.get("data", None)
27+
encoded_body_size = data and data.get("Encoded Body Size", None)
28+
if not (encoded_body_size):
29+
return
30+
31+
payload_size_threshold = self.settings.get("payload_size_threshold")
32+
if encoded_body_size > payload_size_threshold:
33+
self._store_performance_problem(span)
34+
35+
def _store_performance_problem(self, span) -> None:
36+
fingerprint = self._fingerprint(span)
37+
offender_span_ids = span.get("span_id", None)
38+
desc: str = span.get("description", None)
39+
40+
self.stored_problems[fingerprint] = PerformanceProblem(
41+
fingerprint,
42+
"http",
43+
desc=desc,
44+
type=PerformanceLargeHTTPPayloadGroupType,
45+
cause_span_ids=[],
46+
parent_span_ids=None,
47+
offender_span_ids=offender_span_ids,
48+
evidence_display=[],
49+
evidence_data={
50+
"parent_span_ids": [],
51+
"cause_span_ids": [],
52+
"offender_span_ids": offender_span_ids,
53+
"op": "http",
54+
},
55+
)
56+
57+
@classmethod
58+
def _is_span_eligible(cls, span: Span) -> bool:
59+
span_id = span.get("span_id", None)
60+
op: str = span.get("op", "") or ""
61+
hash = span.get("hash", None)
62+
description: str = span.get("description", "") or ""
63+
64+
if not span_id or not op or not hash or not description:
65+
return False
66+
67+
normalized_description = description.strip().upper()
68+
69+
if not normalized_description.startswith(
70+
("GET", "POST", "DELETE", "PUT", "PATCH")
71+
): # Just using all methods to see if anything interesting pops up
72+
return False
73+
74+
if normalized_description.endswith(
75+
(".JS", ".CSS", ".SVG", ".PNG", ".MP3", ".JPG", ".JPEG")
76+
):
77+
return False
78+
79+
if any([x in description for x in ["_next/static/", "_next/data/"]]):
80+
return False
81+
82+
return True
83+
84+
def _fingerprint(self, span) -> str:
85+
hashed_url_paths = fingerprint_http_spans([span])
86+
return f"1-{PerformanceLargeHTTPPayloadGroupType.type_id}-{hashed_url_paths}"
87+
88+
def is_creation_allowed_for_organization(self, organization: Organization) -> bool:
89+
return features.has(
90+
"organizations:performance-large-http-payload-detector", organization, actor=None
91+
)
92+
93+
def is_creation_allowed_for_project(self, project: Project) -> bool:
94+
return True

src/sentry/utils/performance_issues/performance_detection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ConsecutiveHTTPSpanDetector,
2222
DBMainThreadDetector,
2323
FileIOMainThreadDetector,
24+
LargeHTTPPayloadDetector,
2425
MNPlusOneDBSpanDetector,
2526
NPlusOneAPICallsDetector,
2627
NPlusOneDBSpanDetector,
@@ -231,6 +232,7 @@ def get_detection_settings(project_id: Optional[int] = None) -> Dict[DetectorTyp
231232
"consecutive_count_threshold": 3,
232233
"max_duration_between_spans": 10000, # ms
233234
},
235+
DetectorType.LARGE_HTTP_PAYLOAD: {"payload_size_threshold": 10000000}, # 10mb
234236
}
235237

236238

@@ -253,6 +255,7 @@ def _detect_performance_problems(
253255
NPlusOneAPICallsDetector(detection_settings, data),
254256
MNPlusOneDBSpanDetector(detection_settings, data),
255257
UncompressedAssetSpanDetector(detection_settings, data),
258+
LargeHTTPPayloadDetector(detection_settings, data),
256259
]
257260

258261
for detector in detectors:
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from typing import List
2+
3+
import pytest
4+
5+
from sentry.eventstore.models import Event
6+
from sentry.issues.grouptype import PerformanceLargeHTTPPayloadGroupType
7+
from sentry.testutils import TestCase
8+
from sentry.testutils.performance_issues.event_generators import create_event, create_span
9+
from sentry.testutils.silo import region_silo_test
10+
from sentry.utils.performance_issues.detectors import LargeHTTPPayloadDetector
11+
from sentry.utils.performance_issues.performance_detection import (
12+
PerformanceProblem,
13+
get_detection_settings,
14+
run_detector_on_data,
15+
)
16+
17+
18+
@region_silo_test
19+
@pytest.mark.django_db
20+
class LargeHTTPPayloadDetectorTest(TestCase):
21+
def setUp(self):
22+
super().setUp()
23+
self.settings = get_detection_settings()
24+
25+
def find_problems(self, event: Event) -> List[PerformanceProblem]:
26+
detector = LargeHTTPPayloadDetector(self.settings, event)
27+
run_detector_on_data(detector, event)
28+
return list(detector.stored_problems.values())
29+
30+
def test_detects_large_http_payload_issue(self):
31+
32+
spans = [
33+
create_span(
34+
"http.client",
35+
1000,
36+
"GET /api/0/organizations/endpoint1",
37+
"hash1",
38+
data={
39+
"Transfer Size": 50_000_000,
40+
"Encoded Body Size": 50_000_000,
41+
"Decoded Body Size": 50_000_000,
42+
},
43+
)
44+
]
45+
46+
event = create_event(spans)
47+
assert self.find_problems(event) == [
48+
PerformanceProblem(
49+
fingerprint="1-1015-5e5543895c0f1f12c2d468da8c7f2d9e4dca81dc",
50+
op="http",
51+
desc="GET /api/0/organizations/endpoint1",
52+
type=PerformanceLargeHTTPPayloadGroupType,
53+
parent_span_ids=None,
54+
cause_span_ids=[],
55+
offender_span_ids="bbbbbbbbbbbbbbbb",
56+
evidence_data={
57+
"parent_span_ids": [],
58+
"cause_span_ids": [],
59+
"offender_span_ids": "bbbbbbbbbbbbbbbb",
60+
"op": "http",
61+
},
62+
evidence_display=[],
63+
)
64+
]
65+
66+
def test_does_not_issue_if_url_is_an_asset(self):
67+
spans = [
68+
create_span(
69+
"resource.script",
70+
desc="https://s1.sentry-cdn.com/_static/dist/sentry/entrypoints/app.js",
71+
duration=1000.0,
72+
data={
73+
"Transfer Size": 50_000_000,
74+
"Encoded Body Size": 50_000_000,
75+
"Decoded Body Size": 50_000_000,
76+
},
77+
)
78+
]
79+
event = create_event(spans)
80+
assert self.find_problems(event) == []

tests/sentry/utils/performance_issues/test_performance_detection.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ def test_reports_metrics_on_uncompressed_assets(self, incr_mock):
398398
"integration_mongo": False,
399399
"integration_postgres": False,
400400
"consecutive_db": False,
401+
"large_http_payload": False,
401402
"consecutive_http": False,
402403
"slow_db_query": False,
403404
"render_blocking_assets": False,

0 commit comments

Comments
 (0)