diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index b82376e517..5255fcb0fa 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: - from collections.abc import Container, MutableMapping + from collections.abc import Container, MutableMapping, Sequence from datetime import datetime @@ -25,6 +25,11 @@ from typing import Union from typing_extensions import Literal, TypedDict + class SDKInfo(TypedDict): + name: str + version: str + packages: Sequence[Mapping[str, str]] + # "critical" is an alias of "fatal" recognized by Relay LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index edc7b6f7a1..f909532016 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -53,7 +53,7 @@ from typing import Type from typing import Union - from sentry_sdk._types import Event, Hint + from sentry_sdk._types import Event, Hint, SDKInfo from sentry_sdk.integrations import Integration from sentry_sdk.metrics import MetricsAggregator from sentry_sdk.scope import Scope @@ -68,7 +68,7 @@ "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() "version": VERSION, "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], -} +} # type: SDKInfo def _get_options(*args, **kwargs): @@ -386,6 +386,7 @@ def _capture_envelope(envelope): try: setup_continuous_profiler( self.options, + sdk_info=SDK_INFO, capture_func=_capture_envelope, ) except Exception as e: diff --git a/sentry_sdk/profiler/continuous_profiler.py b/sentry_sdk/profiler/continuous_profiler.py index b6f37c43a5..63a9201b6f 100644 --- a/sentry_sdk/profiler/continuous_profiler.py +++ b/sentry_sdk/profiler/continuous_profiler.py @@ -6,6 +6,7 @@ import uuid from datetime import datetime, timezone +from sentry_sdk.consts import VERSION from sentry_sdk.envelope import Envelope from sentry_sdk._lru_cache import LRUCache from sentry_sdk._types import TYPE_CHECKING @@ -31,7 +32,7 @@ from typing import Type from typing import Union from typing_extensions import TypedDict - from sentry_sdk._types import ContinuousProfilerMode + from sentry_sdk._types import ContinuousProfilerMode, SDKInfo from sentry_sdk.profiler.utils import ( ExtractedSample, FrameId, @@ -65,8 +66,8 @@ _scheduler = None # type: Optional[ContinuousScheduler] -def setup_continuous_profiler(options, capture_func): - # type: (Dict[str, Any], Callable[[Envelope], None]) -> bool +def setup_continuous_profiler(options, sdk_info, capture_func): + # type: (Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> bool global _scheduler if _scheduler is not None: @@ -91,9 +92,13 @@ def setup_continuous_profiler(options, capture_func): frequency = DEFAULT_SAMPLING_FREQUENCY if profiler_mode == ThreadContinuousScheduler.mode: - _scheduler = ThreadContinuousScheduler(frequency, options, capture_func) + _scheduler = ThreadContinuousScheduler( + frequency, options, sdk_info, capture_func + ) elif profiler_mode == GeventContinuousScheduler.mode: - _scheduler = GeventContinuousScheduler(frequency, options, capture_func) + _scheduler = GeventContinuousScheduler( + frequency, options, sdk_info, capture_func + ) else: raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) @@ -162,10 +167,11 @@ def get_profiler_id(): class ContinuousScheduler(object): mode = "unknown" # type: ContinuousProfilerMode - def __init__(self, frequency, options, capture_func): - # type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None + def __init__(self, frequency, options, sdk_info, capture_func): + # type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None self.interval = 1.0 / frequency self.options = options + self.sdk_info = sdk_info self.capture_func = capture_func self.sampler = self.make_sampler() self.buffer = None # type: Optional[ProfileBuffer] @@ -194,7 +200,7 @@ def pause(self): def reset_buffer(self): # type: () -> None self.buffer = ProfileBuffer( - self.options, PROFILE_BUFFER_SECONDS, self.capture_func + self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func ) @property @@ -266,9 +272,9 @@ class ThreadContinuousScheduler(ContinuousScheduler): mode = "thread" # type: ContinuousProfilerMode name = "sentry.profiler.ThreadContinuousScheduler" - def __init__(self, frequency, options, capture_func): - # type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None - super().__init__(frequency, options, capture_func) + def __init__(self, frequency, options, sdk_info, capture_func): + # type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None + super().__init__(frequency, options, sdk_info, capture_func) self.thread = None # type: Optional[threading.Thread] self.pid = None # type: Optional[int] @@ -341,13 +347,13 @@ class GeventContinuousScheduler(ContinuousScheduler): mode = "gevent" # type: ContinuousProfilerMode - def __init__(self, frequency, options, capture_func): - # type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None + def __init__(self, frequency, options, sdk_info, capture_func): + # type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None if ThreadPool is None: raise ValueError("Profiler mode: {} is not available".format(self.mode)) - super().__init__(frequency, options, capture_func) + super().__init__(frequency, options, sdk_info, capture_func) self.thread = None # type: Optional[_ThreadPool] self.pid = None # type: Optional[int] @@ -405,9 +411,10 @@ def teardown(self): class ProfileBuffer(object): - def __init__(self, options, buffer_size, capture_func): - # type: (Dict[str, Any], int, Callable[[Envelope], None]) -> None + def __init__(self, options, sdk_info, buffer_size, capture_func): + # type: (Dict[str, Any], SDKInfo, int, Callable[[Envelope], None]) -> None self.options = options + self.sdk_info = sdk_info self.buffer_size = buffer_size self.capture_func = capture_func @@ -445,7 +452,7 @@ def should_flush(self, monotonic_time): def flush(self): # type: () -> None - chunk = self.chunk.to_json(self.profiler_id, self.options) + chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) envelope = Envelope() envelope.add_profile_chunk(chunk) self.capture_func(envelope) @@ -491,8 +498,8 @@ def write(self, ts, sample): # When this happens, we abandon the current sample as it's bad. capture_internal_exception(sys.exc_info()) - def to_json(self, profiler_id, options): - # type: (str, Dict[str, Any]) -> Dict[str, Any] + def to_json(self, profiler_id, options, sdk_info): + # type: (str, Dict[str, Any], SDKInfo) -> Dict[str, Any] profile = { "frames": self.frames, "stacks": self.stacks, @@ -514,6 +521,10 @@ def to_json(self, profiler_id, options): payload = { "chunk_id": self.chunk_id, + "client_sdk": { + "name": sdk_info["name"], + "version": VERSION, + }, "platform": "python", "profile": profile, "profiler_id": profiler_id, diff --git a/tests/profiler/test_continuous_profiler.py b/tests/profiler/test_continuous_profiler.py index 9cf5dadc8d..de647a6a45 100644 --- a/tests/profiler/test_continuous_profiler.py +++ b/tests/profiler/test_continuous_profiler.py @@ -6,6 +6,7 @@ import pytest import sentry_sdk +from sentry_sdk.consts import VERSION from sentry_sdk.profiler.continuous_profiler import ( setup_continuous_profiler, start_profiler, @@ -31,6 +32,13 @@ def experimental_options(mode=None, auto_start=None): } +mock_sdk_info = { + "name": "sentry.python", + "version": VERSION, + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], +} + + @pytest.mark.parametrize("mode", [pytest.param("foo")]) @pytest.mark.parametrize( "make_options", @@ -38,7 +46,11 @@ def experimental_options(mode=None, auto_start=None): ) def test_continuous_profiler_invalid_mode(mode, make_options, teardown_profiling): with pytest.raises(ValueError): - setup_continuous_profiler(make_options(mode=mode), lambda envelope: None) + setup_continuous_profiler( + make_options(mode=mode), + mock_sdk_info, + lambda envelope: None, + ) @pytest.mark.parametrize( @@ -54,7 +66,11 @@ def test_continuous_profiler_invalid_mode(mode, make_options, teardown_profiling ) def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling): options = make_options(mode=mode) - setup_continuous_profiler(options, lambda envelope: None) + setup_continuous_profiler( + options, + mock_sdk_info, + lambda envelope: None, + ) @pytest.mark.parametrize( @@ -71,9 +87,17 @@ def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling): def test_continuous_profiler_setup_twice(mode, make_options, teardown_profiling): options = make_options(mode=mode) # setting up the first time should return True to indicate success - assert setup_continuous_profiler(options, lambda envelope: None) + assert setup_continuous_profiler( + options, + mock_sdk_info, + lambda envelope: None, + ) # setting up the second time should return False to indicate no-op - assert not setup_continuous_profiler(options, lambda envelope: None) + assert not setup_continuous_profiler( + options, + mock_sdk_info, + lambda envelope: None, + ) def assert_single_transaction_with_profile_chunks(envelopes, thread): @@ -119,7 +143,15 @@ def assert_single_transaction_with_profile_chunks(envelopes, thread): for profile_chunk_item in items["profile_chunk"]: profile_chunk = profile_chunk_item.payload.json assert profile_chunk == ApproxDict( - {"platform": "python", "profiler_id": profiler_id, "version": "2"} + { + "client_sdk": { + "name": mock.ANY, + "version": VERSION, + }, + "platform": "python", + "profiler_id": profiler_id, + "version": "2", + } )