Skip to content

Commit 5fc28a1

Browse files
authored
Add org_id support (#5166)
### Description implement part 1 of https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation * Parse `org_id` in `Dsn` constructor * Expose`parsed_dsn` property on `Client` and use it in some places to avoid parsing `Dsn` again * New explicit init option `org_id` that overrides the `org_id` from the `parsed_dsn` if provided * Add `org_id` onto populated DSC as head SDK #### Issues * part of: #5066 * part of: PY-1963
1 parent 828e513 commit 5fc28a1

File tree

9 files changed

+119
-26
lines changed

9 files changed

+119
-26
lines changed

sentry_sdk/client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from sentry_sdk.transport import Transport, Item
7171
from sentry_sdk._log_batcher import LogBatcher
7272
from sentry_sdk._metrics_batcher import MetricsBatcher
73+
from sentry_sdk.utils import Dsn
7374

7475
I = TypeVar("I", bound=Integration) # noqa: E741
7576

@@ -201,6 +202,11 @@ def dsn(self):
201202
# type: () -> Optional[str]
202203
return None
203204

205+
@property
206+
def parsed_dsn(self):
207+
# type: () -> Optional[Dsn]
208+
return None
209+
204210
def should_send_default_pii(self):
205211
# type: () -> bool
206212
return False
@@ -512,6 +518,12 @@ def dsn(self):
512518
"""Returns the configured DSN as string."""
513519
return self.options["dsn"]
514520

521+
@property
522+
def parsed_dsn(self):
523+
# type: () -> Optional[Dsn]
524+
"""Returns the configured parsed DSN object."""
525+
return self.transport.parsed_dsn if self.transport else None
526+
515527
def _prepare_event(
516528
self,
517529
event, # type: Event

sentry_sdk/consts.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,7 @@ def __init__(
10231023
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
10241024
enable_metrics=True, # type: bool
10251025
before_send_metric=None, # type: Optional[Callable[[Metric, Hint], Optional[Metric]]]
1026+
org_id=None, # type: Optional[str]
10261027
):
10271028
# type: (...) -> None
10281029
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
@@ -1426,6 +1427,10 @@ def __init__(
14261427
If `trace_ignore_status_codes` is not provided, requests with any status code
14271428
may be traced.
14281429
1430+
:param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases
1431+
but you can provide it explicitly for self-hosted and Relay setups. This value is used for
1432+
trace propagation and for features like `strict_trace_continuation`.
1433+
14291434
:param _experiments:
14301435
"""
14311436
pass

sentry_sdk/integrations/opentelemetry/span_processor.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
)
2424
from sentry_sdk.scope import add_global_event_processor
2525
from sentry_sdk.tracing import Transaction, Span as SentrySpan
26-
from sentry_sdk.utils import Dsn
2726

2827
from urllib3.util import parse_url as urlparse
2928

@@ -113,12 +112,7 @@ def on_start(self, otel_span, parent_context=None):
113112
# type: (OTelSpan, Optional[context_api.Context]) -> None
114113
client = get_client()
115114

116-
if not client.dsn:
117-
return
118-
119-
try:
120-
_ = Dsn(client.dsn)
121-
except Exception:
115+
if not client.parsed_dsn:
122116
return
123117

124118
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
@@ -233,10 +227,8 @@ def _is_sentry_span(self, otel_span):
233227
otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL)
234228
otel_span_url = cast("Optional[str]", otel_span_url)
235229

236-
dsn_url = None
237-
client = get_client()
238-
if client.dsn:
239-
dsn_url = Dsn(client.dsn).netloc
230+
parsed_dsn = get_client().parsed_dsn
231+
dsn_url = parsed_dsn.netloc if parsed_dsn else None
240232

241233
if otel_span_url and dsn_url and dsn_url in otel_span_url:
242234
return True

sentry_sdk/tracing_utils.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,10 @@ def from_options(cls, scope):
664664
if options.get("release"):
665665
sentry_items["release"] = options["release"]
666666

667-
if options.get("dsn"):
668-
sentry_items["public_key"] = Dsn(options["dsn"]).public_key
667+
if client.parsed_dsn:
668+
sentry_items["public_key"] = client.parsed_dsn.public_key
669+
if client.parsed_dsn.org_id:
670+
sentry_items["org_id"] = client.parsed_dsn.org_id
669671

670672
if options.get("traces_sample_rate"):
671673
sentry_items["sample_rate"] = str(options["traces_sample_rate"])
@@ -696,8 +698,10 @@ def populate_from_transaction(cls, transaction):
696698
if options.get("release"):
697699
sentry_items["release"] = options["release"]
698700

699-
if options.get("dsn"):
700-
sentry_items["public_key"] = Dsn(options["dsn"]).public_key
701+
if client.parsed_dsn:
702+
sentry_items["public_key"] = client.parsed_dsn.public_key
703+
if client.parsed_dsn.org_id:
704+
sentry_items["org_id"] = client.parsed_dsn.org_id
701705

702706
if (
703707
transaction.name

sentry_sdk/transport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(self, options=None):
7070
# type: (Self, Optional[Dict[str, Any]]) -> None
7171
self.options = options
7272
if options and options["dsn"] is not None and options["dsn"]:
73-
self.parsed_dsn = Dsn(options["dsn"])
73+
self.parsed_dsn = Dsn(options["dsn"], options.get("org_id"))
7474
else:
7575
self.parsed_dsn = None
7676

sentry_sdk/utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,10 @@ class BadDsn(ValueError):
294294
class Dsn:
295295
"""Represents a DSN."""
296296

297-
def __init__(self, value):
298-
# type: (Union[Dsn, str]) -> None
297+
ORG_ID_REGEX = re.compile(r"^o(\d+)\.")
298+
299+
def __init__(self, value, org_id=None):
300+
# type: (Union[Dsn, str], Optional[str]) -> None
299301
if isinstance(value, Dsn):
300302
self.__dict__ = dict(value.__dict__)
301303
return
@@ -310,6 +312,12 @@ def __init__(self, value):
310312

311313
self.host = parts.hostname
312314

315+
if org_id is not None:
316+
self.org_id = org_id # type: Optional[str]
317+
else:
318+
org_id_match = Dsn.ORG_ID_REGEX.match(self.host)
319+
self.org_id = org_id_match.group(1) if org_id_match else None
320+
313321
if parts.port is None:
314322
self.port = self.scheme == "https" and 443 or 80 # type: int
315323
else:

tests/integrations/opentelemetry/test_span_processor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SentrySpanProcessor,
1212
link_trace_context_to_error_event,
1313
)
14+
from sentry_sdk.utils import Dsn
1415
from sentry_sdk.tracing import Span, Transaction
1516
from sentry_sdk.tracing_utils import extract_sentrytrace_data
1617

@@ -23,7 +24,7 @@ def test_is_sentry_span():
2324

2425
client = MagicMock()
2526
client.options = {"instrumenter": "otel"}
26-
client.dsn = "https://[email protected]/123456"
27+
client.parsed_dsn = Dsn("https://[email protected]/123456")
2728
sentry_sdk.get_global_scope().set_client(client)
2829

2930
assert not span_processor._is_sentry_span(otel_span)

tests/test_dsc.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,19 @@
1313
import pytest
1414

1515
import sentry_sdk
16-
import sentry_sdk.client
16+
from sentry_sdk.transport import Transport
17+
from sentry_sdk.envelope import Envelope
18+
19+
20+
class TransportWithOptions(Transport):
21+
"""conftest.TestTransport does not pass in the options so we need this here"""
22+
23+
def __init__(self, options=None):
24+
Transport.__init__(self, options)
25+
26+
def capture_envelope(self, _: Envelope) -> None:
27+
"""No-op capture_envelope for tests"""
28+
pass
1729

1830

1931
def test_dsc_head_of_trace(sentry_init, capture_envelopes):
@@ -22,10 +34,11 @@ def test_dsc_head_of_trace(sentry_init, capture_envelopes):
2234
and sends a transaction event to Sentry.
2335
"""
2436
sentry_init(
25-
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
37+
dsn="https://mysecret@o1234.ingest.sentry.io/12312012",
2638
release="[email protected]",
2739
environment="canary",
2840
traces_sample_rate=1.0,
41+
transport=TransportWithOptions,
2942
)
3043
envelopes = capture_envelopes()
3144

@@ -45,6 +58,10 @@ def test_dsc_head_of_trace(sentry_init, capture_envelopes):
4558
assert type(envelope_trace_header["public_key"]) == str
4659
assert envelope_trace_header["public_key"] == "mysecret"
4760

61+
assert "org_id" in envelope_trace_header
62+
assert type(envelope_trace_header["org_id"]) == str
63+
assert envelope_trace_header["org_id"] == "1234"
64+
4865
assert "sample_rate" in envelope_trace_header
4966
assert type(envelope_trace_header["sample_rate"]) == str
5067
assert envelope_trace_header["sample_rate"] == "1.0"
@@ -66,16 +83,46 @@ def test_dsc_head_of_trace(sentry_init, capture_envelopes):
6683
assert envelope_trace_header["transaction"] == "foo"
6784

6885

86+
def test_dsc_head_of_trace_uses_custom_org_id(sentry_init, capture_envelopes):
87+
"""
88+
Our service is the head of the trace (it starts a new trace)
89+
and sends a transaction event to Sentry.
90+
"""
91+
sentry_init(
92+
dsn="https://[email protected]/12312012",
93+
org_id="9999",
94+
release="[email protected]",
95+
environment="canary",
96+
traces_sample_rate=1.0,
97+
transport=TransportWithOptions,
98+
)
99+
envelopes = capture_envelopes()
100+
101+
# We start a new transaction
102+
with sentry_sdk.start_transaction(name="foo"):
103+
pass
104+
105+
assert len(envelopes) == 1
106+
107+
transaction_envelope = envelopes[0]
108+
envelope_trace_header = transaction_envelope.headers["trace"]
109+
110+
assert "org_id" in envelope_trace_header
111+
assert type(envelope_trace_header["org_id"]) == str
112+
assert envelope_trace_header["org_id"] == "9999"
113+
114+
69115
def test_dsc_continuation_of_trace(sentry_init, capture_envelopes):
70116
"""
71117
Another service calls our service and passes tracing information to us.
72118
Our service is continuing the trace and sends a transaction event to Sentry.
73119
"""
74120
sentry_init(
75-
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
121+
dsn="https://mysecret@o1234.ingest.sentry.io/12312012",
76122
release="[email protected]",
77123
environment="canary",
78124
traces_sample_rate=1.0,
125+
transport=TransportWithOptions,
79126
)
80127
envelopes = capture_envelopes()
81128

@@ -149,10 +196,11 @@ def my_traces_sampler(sampling_context):
149196
return 0.25
150197

151198
sentry_init(
152-
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
199+
dsn="https://mysecret@o1234.ingest.sentry.io/12312012",
153200
release="[email protected]",
154201
environment="canary",
155202
traces_sampler=my_traces_sampler,
203+
transport=TransportWithOptions,
156204
)
157205
envelopes = capture_envelopes()
158206

@@ -219,9 +267,10 @@ def test_dsc_issue(sentry_init, capture_envelopes):
219267
Our service is a standalone service that does not have tracing enabled. Just uses Sentry for error reporting.
220268
"""
221269
sentry_init(
222-
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
270+
dsn="https://mysecret@o1234.ingest.sentry.io/12312012",
223271
release="[email protected]",
224272
environment="canary",
273+
transport=TransportWithOptions,
225274
)
226275
envelopes = capture_envelopes()
227276

@@ -244,6 +293,10 @@ def test_dsc_issue(sentry_init, capture_envelopes):
244293
assert type(envelope_trace_header["public_key"]) == str
245294
assert envelope_trace_header["public_key"] == "mysecret"
246295

296+
assert "org_id" in envelope_trace_header
297+
assert type(envelope_trace_header["org_id"]) == str
298+
assert envelope_trace_header["org_id"] == "1234"
299+
247300
assert "sample_rate" not in envelope_trace_header
248301

249302
assert "sampled" not in envelope_trace_header
@@ -265,10 +318,11 @@ def test_dsc_issue_with_tracing(sentry_init, capture_envelopes):
265318
Envelopes containing errors also have the same DSC than the transaction envelopes.
266319
"""
267320
sentry_init(
268-
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
321+
dsn="https://mysecret@o1234.ingest.sentry.io/12312012",
269322
release="[email protected]",
270323
environment="canary",
271324
traces_sample_rate=1.0,
325+
transport=TransportWithOptions,
272326
)
273327
envelopes = capture_envelopes()
274328

@@ -294,6 +348,10 @@ def test_dsc_issue_with_tracing(sentry_init, capture_envelopes):
294348
assert type(envelope_trace_header["public_key"]) == str
295349
assert envelope_trace_header["public_key"] == "mysecret"
296350

351+
assert "org_id" in envelope_trace_header
352+
assert type(envelope_trace_header["org_id"]) == str
353+
assert envelope_trace_header["org_id"] == "1234"
354+
297355
assert "sample_rate" in envelope_trace_header
298356
assert envelope_trace_header["sample_rate"] == "1.0"
299357
assert type(envelope_trace_header["sample_rate"]) == str
@@ -332,10 +390,11 @@ def test_dsc_issue_twp(sentry_init, capture_envelopes, traces_sample_rate):
332390
(This test would be service B in this scenario)
333391
"""
334392
sentry_init(
335-
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
393+
dsn="https://mysecret@o1234.ingest.sentry.io/12312012",
336394
release="[email protected]",
337395
environment="canary",
338396
traces_sample_rate=traces_sample_rate,
397+
transport=TransportWithOptions,
339398
)
340399
envelopes = capture_envelopes()
341400

tests/utils/test_general.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ def test_parse_dsn_paths(given, expected_envelope):
119119
assert auth.get_api_url(EndpointType.ENVELOPE) == expected_envelope
120120

121121

122+
@pytest.mark.parametrize(
123+
"given,expected",
124+
[
125+
("https://[email protected]/123", None),
126+
("https://[email protected]/123", "1234"),
127+
],
128+
)
129+
def test_parse_dsn_org_id(given, expected):
130+
dsn = Dsn(given)
131+
assert dsn.org_id == expected
132+
133+
122134
@pytest.mark.parametrize(
123135
"dsn",
124136
[

0 commit comments

Comments
 (0)