Skip to content

Commit 1778130

Browse files
authored
Merge branch 'master' into ivana/update-sample-rate
2 parents feb2c13 + 797e82f commit 1778130

File tree

10 files changed

+282
-147
lines changed

10 files changed

+282
-147
lines changed

sentry_sdk/_types.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,88 @@
1-
from typing import TYPE_CHECKING
1+
from typing import TYPE_CHECKING, TypeVar, Union
22

33

44
# Re-exported for compat, since code out there in the wild might use this variable.
55
MYPY = TYPE_CHECKING
66

77

8+
SENSITIVE_DATA_SUBSTITUTE = "[Filtered]"
9+
10+
11+
class AnnotatedValue:
12+
"""
13+
Meta information for a data field in the event payload.
14+
This is to tell Relay that we have tampered with the fields value.
15+
See:
16+
https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423
17+
"""
18+
19+
__slots__ = ("value", "metadata")
20+
21+
def __init__(self, value, metadata):
22+
# type: (Optional[Any], Dict[str, Any]) -> None
23+
self.value = value
24+
self.metadata = metadata
25+
26+
def __eq__(self, other):
27+
# type: (Any) -> bool
28+
if not isinstance(other, AnnotatedValue):
29+
return False
30+
31+
return self.value == other.value and self.metadata == other.metadata
32+
33+
@classmethod
34+
def removed_because_raw_data(cls):
35+
# type: () -> AnnotatedValue
36+
"""The value was removed because it could not be parsed. This is done for request body values that are not json nor a form."""
37+
return AnnotatedValue(
38+
value="",
39+
metadata={
40+
"rem": [ # Remark
41+
[
42+
"!raw", # Unparsable raw data
43+
"x", # The fields original value was removed
44+
]
45+
]
46+
},
47+
)
48+
49+
@classmethod
50+
def removed_because_over_size_limit(cls):
51+
# type: () -> AnnotatedValue
52+
"""The actual value was removed because the size of the field exceeded the configured maximum size (specified with the max_request_body_size sdk option)"""
53+
return AnnotatedValue(
54+
value="",
55+
metadata={
56+
"rem": [ # Remark
57+
[
58+
"!config", # Because of configured maximum size
59+
"x", # The fields original value was removed
60+
]
61+
]
62+
},
63+
)
64+
65+
@classmethod
66+
def substituted_because_contains_sensitive_data(cls):
67+
# type: () -> AnnotatedValue
68+
"""The actual value was removed because it contained sensitive information."""
69+
return AnnotatedValue(
70+
value=SENSITIVE_DATA_SUBSTITUTE,
71+
metadata={
72+
"rem": [ # Remark
73+
[
74+
"!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies)
75+
"s", # The fields original value was substituted
76+
]
77+
]
78+
},
79+
)
80+
81+
82+
T = TypeVar("T")
83+
Annotated = Union[AnnotatedValue, T]
84+
85+
886
if TYPE_CHECKING:
987
from collections.abc import Container, MutableMapping, Sequence
1088

@@ -19,7 +97,6 @@
1997
from typing import Optional
2098
from typing import Tuple
2199
from typing import Type
22-
from typing import Union
23100
from typing_extensions import Literal, TypedDict
24101

25102
class SDKInfo(TypedDict):
@@ -101,7 +178,7 @@ class SDKInfo(TypedDict):
101178
"request": dict[str, object],
102179
"sdk": Mapping[str, object],
103180
"server_name": str,
104-
"spans": list[dict[str, object]],
181+
"spans": Annotated[list[dict[str, object]]],
105182
"stacktrace": dict[
106183
str, object
107184
], # We access this key in the code, but I am unsure whether we ever set it
@@ -118,6 +195,7 @@ class SDKInfo(TypedDict):
118195
"transaction_info": Mapping[str, Any], # TODO: We can expand on this type
119196
"type": Literal["check_in", "transaction"],
120197
"user": dict[str, object],
198+
"_dropped_spans": int,
121199
"_metrics_summary": dict[str, object],
122200
},
123201
total=False,

sentry_sdk/client.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from collections.abc import Mapping
66
from datetime import datetime, timezone
77
from importlib import import_module
8-
from typing import cast, overload
8+
from typing import TYPE_CHECKING, List, Dict, cast, overload
99
import warnings
1010

1111
from sentry_sdk._compat import PY37, check_uwsgi_thread_support
1212
from sentry_sdk.utils import (
13+
AnnotatedValue,
1314
ContextVar,
1415
capture_internal_exceptions,
1516
current_stacktrace,
@@ -45,12 +46,9 @@
4546
from sentry_sdk.monitor import Monitor
4647
from sentry_sdk.spotlight import setup_spotlight
4748

48-
from typing import TYPE_CHECKING
49-
5049
if TYPE_CHECKING:
5150
from typing import Any
5251
from typing import Callable
53-
from typing import Dict
5452
from typing import Optional
5553
from typing import Sequence
5654
from typing import Type
@@ -483,12 +481,14 @@ def _prepare_event(
483481
):
484482
# type: (...) -> Optional[Event]
485483

484+
previous_total_spans = None # type: Optional[int]
485+
486486
if event.get("timestamp") is None:
487487
event["timestamp"] = datetime.now(timezone.utc)
488488

489489
if scope is not None:
490490
is_transaction = event.get("type") == "transaction"
491-
spans_before = len(event.get("spans", []))
491+
spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
492492
event_ = scope.apply_to_event(event, hint, self.options)
493493

494494
# one of the event/error processors returned None
@@ -507,13 +507,18 @@ def _prepare_event(
507507
return None
508508

509509
event = event_
510-
511-
spans_delta = spans_before - len(event.get("spans", []))
510+
spans_delta = spans_before - len(
511+
cast(List[Dict[str, object]], event.get("spans", []))
512+
)
512513
if is_transaction and spans_delta > 0 and self.transport is not None:
513514
self.transport.record_lost_event(
514515
"event_processor", data_category="span", quantity=spans_delta
515516
)
516517

518+
dropped_spans = event.pop("_dropped_spans", 0) + spans_delta # type: int
519+
if dropped_spans > 0:
520+
previous_total_spans = spans_before + dropped_spans
521+
517522
if (
518523
self.options["attach_stacktrace"]
519524
and "exception" not in event
@@ -561,6 +566,11 @@ def _prepare_event(
561566
if event_scrubber:
562567
event_scrubber.scrub_event(event)
563568

569+
if previous_total_spans is not None:
570+
event["spans"] = AnnotatedValue(
571+
event.get("spans", []), {"len": previous_total_spans}
572+
)
573+
564574
# Postprocess the event here so that annotated types do
565575
# generally not surface in before_send
566576
if event is not None:
@@ -598,7 +608,7 @@ def _prepare_event(
598608
and event.get("type") == "transaction"
599609
):
600610
new_event = None
601-
spans_before = len(event.get("spans", []))
611+
spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
602612
with capture_internal_exceptions():
603613
new_event = before_send_transaction(event, hint or {})
604614
if new_event is None:

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ def __init__(
528528
profiles_sample_rate=None, # type: Optional[float]
529529
profiles_sampler=None, # type: Optional[TracesSampler]
530530
profiler_mode=None, # type: Optional[ProfilerMode]
531+
profile_session_sample_rate=None, # type: Optional[float]
531532
auto_enabling_integrations=True, # type: bool
532533
disabled_integrations=None, # type: Optional[Sequence[sentry_sdk.integrations.Integration]]
533534
auto_session_tracking=True, # type: bool

sentry_sdk/profiler/continuous_profiler.py

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import atexit
22
import os
3+
import random
34
import sys
45
import threading
56
import time
@@ -83,11 +84,15 @@ def setup_continuous_profiler(options, sdk_info, capture_func):
8384
else:
8485
default_profiler_mode = ThreadContinuousScheduler.mode
8586

86-
experiments = options.get("_experiments", {})
87+
if options.get("profiler_mode") is not None:
88+
profiler_mode = options["profiler_mode"]
89+
else:
90+
# TODO: deprecate this and just use the existing `profiler_mode`
91+
experiments = options.get("_experiments", {})
8792

88-
profiler_mode = (
89-
experiments.get("continuous_profiling_mode") or default_profiler_mode
90-
)
93+
profiler_mode = (
94+
experiments.get("continuous_profiling_mode") or default_profiler_mode
95+
)
9196

9297
frequency = DEFAULT_SAMPLING_FREQUENCY
9398

@@ -118,35 +123,26 @@ def try_autostart_continuous_profiler():
118123
if _scheduler is None:
119124
return
120125

121-
# Ensure that the scheduler only autostarts once per process.
122-
# This is necessary because many web servers use forks to spawn
123-
# additional processes. And the profiler is only spawned on the
124-
# master process, then it often only profiles the main process
125-
# and not the ones where the requests are being handled.
126-
#
127-
# Additionally, we only want this autostart behaviour once per
128-
# process. If the user explicitly calls `stop_profiler`, it should
129-
# be respected and not start the profiler again.
130-
if not _scheduler.should_autostart():
126+
if not _scheduler.is_auto_start_enabled():
131127
return
132128

133-
_scheduler.ensure_running()
129+
_scheduler.manual_start()
134130

135131

136132
def start_profiler():
137133
# type: () -> None
138134
if _scheduler is None:
139135
return
140136

141-
_scheduler.ensure_running()
137+
_scheduler.manual_start()
142138

143139

144140
def stop_profiler():
145141
# type: () -> None
146142
if _scheduler is None:
147143
return
148144

149-
_scheduler.teardown()
145+
_scheduler.manual_stop()
150146

151147

152148
def teardown_continuous_profiler():
@@ -164,6 +160,16 @@ def get_profiler_id():
164160
return _scheduler.profiler_id
165161

166162

163+
def determine_profile_session_sampling_decision(sample_rate):
164+
# type: (Union[float, None]) -> bool
165+
166+
# `None` is treated as `0.0`
167+
if not sample_rate:
168+
return False
169+
170+
return random.random() < float(sample_rate)
171+
172+
167173
class ContinuousScheduler:
168174
mode = "unknown" # type: ContinuousProfilerMode
169175

@@ -175,16 +181,43 @@ def __init__(self, frequency, options, sdk_info, capture_func):
175181
self.capture_func = capture_func
176182
self.sampler = self.make_sampler()
177183
self.buffer = None # type: Optional[ProfileBuffer]
184+
self.pid = None # type: Optional[int]
178185

179186
self.running = False
180187

181-
def should_autostart(self):
188+
profile_session_sample_rate = self.options.get("profile_session_sample_rate")
189+
self.sampled = determine_profile_session_sampling_decision(
190+
profile_session_sample_rate
191+
)
192+
193+
def is_auto_start_enabled(self):
182194
# type: () -> bool
195+
196+
# Ensure that the scheduler only autostarts once per process.
197+
# This is necessary because many web servers use forks to spawn
198+
# additional processes. And the profiler is only spawned on the
199+
# master process, then it often only profiles the main process
200+
# and not the ones where the requests are being handled.
201+
if self.pid == os.getpid():
202+
return False
203+
183204
experiments = self.options.get("_experiments")
184205
if not experiments:
185206
return False
207+
186208
return experiments.get("continuous_profiling_auto_start")
187209

210+
def manual_start(self):
211+
# type: () -> None
212+
if not self.sampled:
213+
return
214+
215+
self.ensure_running()
216+
217+
def manual_stop(self):
218+
# type: () -> None
219+
self.teardown()
220+
188221
def ensure_running(self):
189222
# type: () -> None
190223
raise NotImplementedError
@@ -277,15 +310,11 @@ def __init__(self, frequency, options, sdk_info, capture_func):
277310
super().__init__(frequency, options, sdk_info, capture_func)
278311

279312
self.thread = None # type: Optional[threading.Thread]
280-
self.pid = None # type: Optional[int]
281313
self.lock = threading.Lock()
282314

283-
def should_autostart(self):
284-
# type: () -> bool
285-
return super().should_autostart() and self.pid != os.getpid()
286-
287315
def ensure_running(self):
288316
# type: () -> None
317+
289318
pid = os.getpid()
290319

291320
# is running on the right process
@@ -356,13 +385,8 @@ def __init__(self, frequency, options, sdk_info, capture_func):
356385
super().__init__(frequency, options, sdk_info, capture_func)
357386

358387
self.thread = None # type: Optional[_ThreadPool]
359-
self.pid = None # type: Optional[int]
360388
self.lock = threading.Lock()
361389

362-
def should_autostart(self):
363-
# type: () -> bool
364-
return super().should_autostart() and self.pid != os.getpid()
365-
366390
def ensure_running(self):
367391
# type: () -> None
368392
pid = os.getpid()
@@ -393,7 +417,6 @@ def ensure_running(self):
393417
# longer allows us to spawn a thread and we have to bail.
394418
self.running = False
395419
self.thread = None
396-
return
397420

398421
def teardown(self):
399422
# type: () -> None
@@ -407,7 +430,7 @@ def teardown(self):
407430
self.buffer = None
408431

409432

410-
PROFILE_BUFFER_SECONDS = 10
433+
PROFILE_BUFFER_SECONDS = 60
411434

412435

413436
class ProfileBuffer:

0 commit comments

Comments
 (0)