@@ -24,6 +24,7 @@ class DetectorType(Enum):
24
24
DUPLICATE_SPANS = "duplicates"
25
25
SEQUENTIAL_SLOW_SPANS = "sequential"
26
26
LONG_TASK_SPANS = "long_task"
27
+ RENDER_BLOCKING_ASSET_SPAN = "render_blocking_assets"
27
28
28
29
29
30
# Facade in front of performance detection to limit impact of detection on our events ingestion
@@ -85,6 +86,12 @@ def get_default_detection_settings():
85
86
"allowed_span_ops" : ["ui.long-task" , "ui.sentry.long-task" ],
86
87
}
87
88
],
89
+ DetectorType .RENDER_BLOCKING_ASSET_SPAN : {
90
+ "fcp_minimum_threshold" : 2000.0 , # ms
91
+ "fcp_maximum_threshold" : 10000.0 , # ms
92
+ "fcp_ratio_threshold" : 0.25 ,
93
+ "allowed_span_ops" : ["resource.link" , "resource.script" ],
94
+ },
88
95
}
89
96
90
97
@@ -94,11 +101,14 @@ def _detect_performance_issue(data: Event, sdk_span: Any):
94
101
95
102
detection_settings = get_default_detection_settings ()
96
103
detectors = {
97
- DetectorType .DUPLICATE_SPANS : DuplicateSpanDetector (detection_settings ),
98
- DetectorType .DUPLICATE_SPANS_HASH : DuplicateSpanHashDetector (detection_settings ),
99
- DetectorType .SLOW_SPAN : SlowSpanDetector (detection_settings ),
100
- DetectorType .SEQUENTIAL_SLOW_SPANS : SequentialSlowSpanDetector (detection_settings ),
101
- DetectorType .LONG_TASK_SPANS : LongTaskSpanDetector (detection_settings ),
104
+ DetectorType .DUPLICATE_SPANS : DuplicateSpanDetector (detection_settings , data ),
105
+ DetectorType .DUPLICATE_SPANS_HASH : DuplicateSpanHashDetector (detection_settings , data ),
106
+ DetectorType .SLOW_SPAN : SlowSpanDetector (detection_settings , data ),
107
+ DetectorType .SEQUENTIAL_SLOW_SPANS : SequentialSlowSpanDetector (detection_settings , data ),
108
+ DetectorType .LONG_TASK_SPANS : LongTaskSpanDetector (detection_settings , data ),
109
+ DetectorType .RENDER_BLOCKING_ASSET_SPAN : RenderBlockingAssetSpanDetector (
110
+ detection_settings , data
111
+ ),
102
112
}
103
113
104
114
for span in spans :
@@ -143,8 +153,9 @@ class PerformanceDetector(ABC):
143
153
Classes of this type have their visit functions called as the event is walked once and will store a performance issue if one is detected.
144
154
"""
145
155
146
- def __init__ (self , settings : Dict [str , Any ]):
156
+ def __init__ (self , settings : Dict [str , Any ], event : Event ):
147
157
self .settings = settings [self .settings_key ]
158
+ self ._event = event
148
159
self .init ()
149
160
150
161
@abstractmethod
@@ -170,6 +181,9 @@ def settings_for_span(self, span: Span):
170
181
return op , span_id , op_prefix , span_duration , setting
171
182
return None
172
183
184
+ def event (self ) -> Event :
185
+ return self ._event
186
+
173
187
@property
174
188
@abstractmethod
175
189
def settings_key (self ) -> DetectorType :
@@ -411,6 +425,64 @@ def visit_span(self, span: Span):
411
425
)
412
426
413
427
428
+ class RenderBlockingAssetSpanDetector (PerformanceDetector ):
429
+ __slots__ = ("stored_issues" , "fcp" , "transaction_start" )
430
+
431
+ settings_key = DetectorType .RENDER_BLOCKING_ASSET_SPAN
432
+
433
+ def init (self ):
434
+ self .stored_issues = {}
435
+ self .transaction_start = timedelta (seconds = self .event ().get ("transaction_start" , 0 ))
436
+ self .fcp = None
437
+
438
+ # Only concern ourselves with transactions where the FCP is within the
439
+ # range we care about.
440
+ fcp_hash = self .event ().get ("measurements" , {}).get ("fcp" , {})
441
+ if "value" in fcp_hash and ("unit" not in fcp_hash or fcp_hash ["unit" ] == "millisecond" ):
442
+ fcp = timedelta (milliseconds = fcp_hash .get ("value" ))
443
+ fcp_minimum_threshold = timedelta (
444
+ milliseconds = self .settings .get ("fcp_minimum_threshold" )
445
+ )
446
+ fcp_maximum_threshold = timedelta (
447
+ milliseconds = self .settings .get ("fcp_maximum_threshold" )
448
+ )
449
+ if fcp >= fcp_minimum_threshold and fcp < fcp_maximum_threshold :
450
+ self .fcp = fcp
451
+
452
+ def visit_span (self , span : Span ):
453
+ if not self .fcp :
454
+ return
455
+
456
+ op = span .get ("op" , None )
457
+ allowed_span_ops = self .settings .get ("allowed_span_ops" )
458
+ if op not in allowed_span_ops :
459
+ return False
460
+
461
+ if self ._is_blocking_render (span ):
462
+ span_id = span .get ("span_id" , None )
463
+ fingerprint = fingerprint_span (span )
464
+ if span_id and fingerprint :
465
+ self .stored_issues [fingerprint ] = PerformanceSpanIssue (span_id , op , [span_id ])
466
+
467
+ # If we visit a span that starts after FCP, then we know we've already
468
+ # seen all possible render-blocking resource spans.
469
+ span_start_timestamp = timedelta (seconds = span .get ("start_timestamp" , 0 ))
470
+ fcp_timestamp = self .transaction_start + self .fcp
471
+ if span_start_timestamp >= fcp_timestamp :
472
+ # Early return for all future span visits.
473
+ self .fcp = None
474
+
475
+ def _is_blocking_render (self , span ):
476
+ span_end_timestamp = timedelta (seconds = span .get ("timestamp" , 0 ))
477
+ fcp_timestamp = self .transaction_start + self .fcp
478
+ if span_end_timestamp >= fcp_timestamp :
479
+ return False
480
+
481
+ span_duration = get_span_duration (span )
482
+ fcp_ratio_threshold = self .settings .get ("fcp_ratio_threshold" )
483
+ return span_duration / self .fcp > fcp_ratio_threshold
484
+
485
+
414
486
# Reports metrics and creates spans for detection
415
487
def report_metrics_for_detectors (
416
488
event_id : Optional [str ], detectors : Dict [str , PerformanceDetector ], sdk_span : Any
0 commit comments