Skip to content

Commit 5529c70

Browse files
feat(profiling): Add client sdk info to profile chunk (#3386)
* feat(profiling): Add client sdk info to profile chunk We want to attach the client sdk info for debugging purposes. * address PR comments * use class syntax for typed dict * import Sequence from collections.abc * fix typing --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent 39517b5 commit 5529c70

File tree

4 files changed

+76
-27
lines changed

4 files changed

+76
-27
lines changed

sentry_sdk/_types.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
if TYPE_CHECKING:
12-
from collections.abc import Container, MutableMapping
12+
from collections.abc import Container, MutableMapping, Sequence
1313

1414
from datetime import datetime
1515

@@ -25,6 +25,11 @@
2525
from typing import Union
2626
from typing_extensions import Literal, TypedDict
2727

28+
class SDKInfo(TypedDict):
29+
name: str
30+
version: str
31+
packages: Sequence[Mapping[str, str]]
32+
2833
# "critical" is an alias of "fatal" recognized by Relay
2934
LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"]
3035

sentry_sdk/client.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from typing import Type
5555
from typing import Union
5656

57-
from sentry_sdk._types import Event, Hint
57+
from sentry_sdk._types import Event, Hint, SDKInfo
5858
from sentry_sdk.integrations import Integration
5959
from sentry_sdk.metrics import MetricsAggregator
6060
from sentry_sdk.scope import Scope
@@ -69,7 +69,7 @@
6969
"name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations()
7070
"version": VERSION,
7171
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
72-
}
72+
} # type: SDKInfo
7373

7474

7575
def _get_options(*args, **kwargs):
@@ -391,6 +391,7 @@ def _capture_envelope(envelope):
391391
try:
392392
setup_continuous_profiler(
393393
self.options,
394+
sdk_info=SDK_INFO,
394395
capture_func=_capture_envelope,
395396
)
396397
except Exception as e:

sentry_sdk/profiler/continuous_profiler.py

+30-19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import uuid
77
from datetime import datetime, timezone
88

9+
from sentry_sdk.consts import VERSION
910
from sentry_sdk.envelope import Envelope
1011
from sentry_sdk._lru_cache import LRUCache
1112
from sentry_sdk._types import TYPE_CHECKING
@@ -31,7 +32,7 @@
3132
from typing import Type
3233
from typing import Union
3334
from typing_extensions import TypedDict
34-
from sentry_sdk._types import ContinuousProfilerMode
35+
from sentry_sdk._types import ContinuousProfilerMode, SDKInfo
3536
from sentry_sdk.profiler.utils import (
3637
ExtractedSample,
3738
FrameId,
@@ -65,8 +66,8 @@
6566
_scheduler = None # type: Optional[ContinuousScheduler]
6667

6768

68-
def setup_continuous_profiler(options, capture_func):
69-
# type: (Dict[str, Any], Callable[[Envelope], None]) -> bool
69+
def setup_continuous_profiler(options, sdk_info, capture_func):
70+
# type: (Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> bool
7071
global _scheduler
7172

7273
if _scheduler is not None:
@@ -91,9 +92,13 @@ def setup_continuous_profiler(options, capture_func):
9192
frequency = DEFAULT_SAMPLING_FREQUENCY
9293

9394
if profiler_mode == ThreadContinuousScheduler.mode:
94-
_scheduler = ThreadContinuousScheduler(frequency, options, capture_func)
95+
_scheduler = ThreadContinuousScheduler(
96+
frequency, options, sdk_info, capture_func
97+
)
9598
elif profiler_mode == GeventContinuousScheduler.mode:
96-
_scheduler = GeventContinuousScheduler(frequency, options, capture_func)
99+
_scheduler = GeventContinuousScheduler(
100+
frequency, options, sdk_info, capture_func
101+
)
97102
else:
98103
raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode))
99104

@@ -162,10 +167,11 @@ def get_profiler_id():
162167
class ContinuousScheduler(object):
163168
mode = "unknown" # type: ContinuousProfilerMode
164169

165-
def __init__(self, frequency, options, capture_func):
166-
# type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None
170+
def __init__(self, frequency, options, sdk_info, capture_func):
171+
# type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None
167172
self.interval = 1.0 / frequency
168173
self.options = options
174+
self.sdk_info = sdk_info
169175
self.capture_func = capture_func
170176
self.sampler = self.make_sampler()
171177
self.buffer = None # type: Optional[ProfileBuffer]
@@ -194,7 +200,7 @@ def pause(self):
194200
def reset_buffer(self):
195201
# type: () -> None
196202
self.buffer = ProfileBuffer(
197-
self.options, PROFILE_BUFFER_SECONDS, self.capture_func
203+
self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func
198204
)
199205

200206
@property
@@ -266,9 +272,9 @@ class ThreadContinuousScheduler(ContinuousScheduler):
266272
mode = "thread" # type: ContinuousProfilerMode
267273
name = "sentry.profiler.ThreadContinuousScheduler"
268274

269-
def __init__(self, frequency, options, capture_func):
270-
# type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None
271-
super().__init__(frequency, options, capture_func)
275+
def __init__(self, frequency, options, sdk_info, capture_func):
276+
# type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None
277+
super().__init__(frequency, options, sdk_info, capture_func)
272278

273279
self.thread = None # type: Optional[threading.Thread]
274280
self.pid = None # type: Optional[int]
@@ -341,13 +347,13 @@ class GeventContinuousScheduler(ContinuousScheduler):
341347

342348
mode = "gevent" # type: ContinuousProfilerMode
343349

344-
def __init__(self, frequency, options, capture_func):
345-
# type: (int, Dict[str, Any], Callable[[Envelope], None]) -> None
350+
def __init__(self, frequency, options, sdk_info, capture_func):
351+
# type: (int, Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> None
346352

347353
if ThreadPool is None:
348354
raise ValueError("Profiler mode: {} is not available".format(self.mode))
349355

350-
super().__init__(frequency, options, capture_func)
356+
super().__init__(frequency, options, sdk_info, capture_func)
351357

352358
self.thread = None # type: Optional[_ThreadPool]
353359
self.pid = None # type: Optional[int]
@@ -405,9 +411,10 @@ def teardown(self):
405411

406412

407413
class ProfileBuffer(object):
408-
def __init__(self, options, buffer_size, capture_func):
409-
# type: (Dict[str, Any], int, Callable[[Envelope], None]) -> None
414+
def __init__(self, options, sdk_info, buffer_size, capture_func):
415+
# type: (Dict[str, Any], SDKInfo, int, Callable[[Envelope], None]) -> None
410416
self.options = options
417+
self.sdk_info = sdk_info
411418
self.buffer_size = buffer_size
412419
self.capture_func = capture_func
413420

@@ -445,7 +452,7 @@ def should_flush(self, monotonic_time):
445452

446453
def flush(self):
447454
# type: () -> None
448-
chunk = self.chunk.to_json(self.profiler_id, self.options)
455+
chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info)
449456
envelope = Envelope()
450457
envelope.add_profile_chunk(chunk)
451458
self.capture_func(envelope)
@@ -491,8 +498,8 @@ def write(self, ts, sample):
491498
# When this happens, we abandon the current sample as it's bad.
492499
capture_internal_exception(sys.exc_info())
493500

494-
def to_json(self, profiler_id, options):
495-
# type: (str, Dict[str, Any]) -> Dict[str, Any]
501+
def to_json(self, profiler_id, options, sdk_info):
502+
# type: (str, Dict[str, Any], SDKInfo) -> Dict[str, Any]
496503
profile = {
497504
"frames": self.frames,
498505
"stacks": self.stacks,
@@ -514,6 +521,10 @@ def to_json(self, profiler_id, options):
514521

515522
payload = {
516523
"chunk_id": self.chunk_id,
524+
"client_sdk": {
525+
"name": sdk_info["name"],
526+
"version": VERSION,
527+
},
517528
"platform": "python",
518529
"profile": profile,
519530
"profiler_id": profiler_id,

tests/profiler/test_continuous_profiler.py

+37-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
import sentry_sdk
9+
from sentry_sdk.consts import VERSION
910
from sentry_sdk.profiler.continuous_profiler import (
1011
setup_continuous_profiler,
1112
start_profiler,
@@ -31,14 +32,25 @@ def experimental_options(mode=None, auto_start=None):
3132
}
3233

3334

35+
mock_sdk_info = {
36+
"name": "sentry.python",
37+
"version": VERSION,
38+
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
39+
}
40+
41+
3442
@pytest.mark.parametrize("mode", [pytest.param("foo")])
3543
@pytest.mark.parametrize(
3644
"make_options",
3745
[pytest.param(experimental_options, id="experiment")],
3846
)
3947
def test_continuous_profiler_invalid_mode(mode, make_options, teardown_profiling):
4048
with pytest.raises(ValueError):
41-
setup_continuous_profiler(make_options(mode=mode), lambda envelope: None)
49+
setup_continuous_profiler(
50+
make_options(mode=mode),
51+
mock_sdk_info,
52+
lambda envelope: None,
53+
)
4254

4355

4456
@pytest.mark.parametrize(
@@ -54,7 +66,11 @@ def test_continuous_profiler_invalid_mode(mode, make_options, teardown_profiling
5466
)
5567
def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling):
5668
options = make_options(mode=mode)
57-
setup_continuous_profiler(options, lambda envelope: None)
69+
setup_continuous_profiler(
70+
options,
71+
mock_sdk_info,
72+
lambda envelope: None,
73+
)
5874

5975

6076
@pytest.mark.parametrize(
@@ -71,9 +87,17 @@ def test_continuous_profiler_valid_mode(mode, make_options, teardown_profiling):
7187
def test_continuous_profiler_setup_twice(mode, make_options, teardown_profiling):
7288
options = make_options(mode=mode)
7389
# setting up the first time should return True to indicate success
74-
assert setup_continuous_profiler(options, lambda envelope: None)
90+
assert setup_continuous_profiler(
91+
options,
92+
mock_sdk_info,
93+
lambda envelope: None,
94+
)
7595
# setting up the second time should return False to indicate no-op
76-
assert not setup_continuous_profiler(options, lambda envelope: None)
96+
assert not setup_continuous_profiler(
97+
options,
98+
mock_sdk_info,
99+
lambda envelope: None,
100+
)
77101

78102

79103
def assert_single_transaction_with_profile_chunks(envelopes, thread):
@@ -119,7 +143,15 @@ def assert_single_transaction_with_profile_chunks(envelopes, thread):
119143
for profile_chunk_item in items["profile_chunk"]:
120144
profile_chunk = profile_chunk_item.payload.json
121145
assert profile_chunk == ApproxDict(
122-
{"platform": "python", "profiler_id": profiler_id, "version": "2"}
146+
{
147+
"client_sdk": {
148+
"name": mock.ANY,
149+
"version": VERSION,
150+
},
151+
"platform": "python",
152+
"profiler_id": profiler_id,
153+
"version": "2",
154+
}
123155
)
124156

125157

0 commit comments

Comments
 (0)