Skip to content

Commit 79a9407

Browse files
authored
Merge 96a5ff2 into 9054d65
2 parents 9054d65 + 96a5ff2 commit 79a9407

File tree

9 files changed

+324
-2
lines changed

9 files changed

+324
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Add strict trace continuation support ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
78
- Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100))
89
- OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint.
910
- Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation.

sentry/api/sentry.api

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public final class io/sentry/Baggage {
4747
public static fun fromHeader (Ljava/util/List;ZLio/sentry/ILogger;)Lio/sentry/Baggage;
4848
public fun get (Ljava/lang/String;)Ljava/lang/String;
4949
public fun getEnvironment ()Ljava/lang/String;
50+
public fun getOrgId ()Ljava/lang/String;
5051
public fun getPublicKey ()Ljava/lang/String;
5152
public fun getRelease ()Ljava/lang/String;
5253
public fun getReplayId ()Ljava/lang/String;
@@ -62,6 +63,7 @@ public final class io/sentry/Baggage {
6263
public fun isShouldFreeze ()Z
6364
public fun set (Ljava/lang/String;Ljava/lang/String;)V
6465
public fun setEnvironment (Ljava/lang/String;)V
66+
public fun setOrgId (Ljava/lang/String;)V
6567
public fun setPublicKey (Ljava/lang/String;)V
6668
public fun setRelease (Ljava/lang/String;)V
6769
public fun setReplayId (Ljava/lang/String;)V
@@ -81,6 +83,7 @@ public final class io/sentry/Baggage {
8183
public final class io/sentry/Baggage$DSCKeys {
8284
public static final field ALL Ljava/util/List;
8385
public static final field ENVIRONMENT Ljava/lang/String;
86+
public static final field ORG_ID Ljava/lang/String;
8487
public static final field PUBLIC_KEY Ljava/lang/String;
8588
public static final field RELEASE Ljava/lang/String;
8689
public static final field REPLAY_ID Ljava/lang/String;
@@ -2230,6 +2233,7 @@ public final class io/sentry/PropagationContext {
22302233
public static fun fromExistingTrace (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)Lio/sentry/PropagationContext;
22312234
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/lang/String;)Lio/sentry/PropagationContext;
22322235
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/util/List;)Lio/sentry/PropagationContext;
2236+
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/util/List;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext;
22332237
public static fun fromHeaders (Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;Lio/sentry/SpanId;)Lio/sentry/PropagationContext;
22342238
public fun getBaggage ()Lio/sentry/Baggage;
22352239
public fun getParentSpanId ()Lio/sentry/SpanId;
@@ -3510,6 +3514,7 @@ public class io/sentry/SentryOptions {
35103514
public fun getDistribution ()Lio/sentry/SentryOptions$DistributionOptions;
35113515
public fun getDistributionController ()Lio/sentry/IDistributionApi;
35123516
public fun getDsn ()Ljava/lang/String;
3517+
public fun getEffectiveOrgId ()Ljava/lang/String;
35133518
public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache;
35143519
public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader;
35153520
public fun getEnvironment ()Ljava/lang/String;
@@ -3550,6 +3555,7 @@ public class io/sentry/SentryOptions {
35503555
public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback;
35513556
public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode;
35523557
public fun getOptionsObservers ()Ljava/util/List;
3558+
public fun getOrgId ()Ljava/lang/String;
35533559
public fun getOutboxPath ()Ljava/lang/String;
35543560
public fun getPerformanceCollectors ()Ljava/util/List;
35553561
public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle;
@@ -3621,6 +3627,7 @@ public class io/sentry/SentryOptions {
36213627
public fun isSendDefaultPii ()Z
36223628
public fun isSendModules ()Z
36233629
public fun isStartProfilerOnAppStart ()Z
3630+
public fun isStrictTraceContinuation ()Z
36243631
public fun isTraceOptionsRequests ()Z
36253632
public fun isTraceSampling ()Z
36263633
public fun isTracingEnabled ()Z
@@ -3704,6 +3711,7 @@ public class io/sentry/SentryOptions {
37043711
public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V
37053712
public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V
37063713
public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V
3714+
public fun setOrgId (Ljava/lang/String;)V
37073715
public fun setPrintUncaughtStackTrace (Z)V
37083716
public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V
37093717
public fun setProfileSessionSampleRate (Ljava/lang/Double;)V
@@ -3736,6 +3744,7 @@ public class io/sentry/SentryOptions {
37363744
public fun setSpotlightConnectionUrl (Ljava/lang/String;)V
37373745
public fun setSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)V
37383746
public fun setStartProfilerOnAppStart (Z)V
3747+
public fun setStrictTraceContinuation (Z)V
37393748
public fun setTag (Ljava/lang/String;Ljava/lang/String;)V
37403749
public fun setThreadChecker (Lio/sentry/util/thread/IThreadChecker;)V
37413750
public fun setTraceOptionsRequests (Z)V

sentry/src/main/java/io/sentry/Baggage.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ public static Baggage fromEvent(
186186
baggage.setPublicKey(options.retrieveParsedDsn().getPublicKey());
187187
baggage.setRelease(event.getRelease());
188188
baggage.setEnvironment(event.getEnvironment());
189+
baggage.setOrgId(options.getEffectiveOrgId());
189190
baggage.setTransaction(transaction);
190191
// we don't persist sample rate
191192
baggage.setSampleRate(null);
@@ -450,6 +451,16 @@ public void setReplayId(final @Nullable String replayId) {
450451
set(DSCKeys.REPLAY_ID, replayId);
451452
}
452453

454+
@ApiStatus.Internal
455+
public @Nullable String getOrgId() {
456+
return get(DSCKeys.ORG_ID);
457+
}
458+
459+
@ApiStatus.Internal
460+
public void setOrgId(final @Nullable String orgId) {
461+
set(DSCKeys.ORG_ID, orgId);
462+
}
463+
453464
/**
454465
* Sets / updates a value, but only if the baggage is still mutable.
455466
*
@@ -501,6 +512,7 @@ public void setValuesFromTransaction(
501512
if (replayId != null && !SentryId.EMPTY_ID.equals(replayId)) {
502513
setReplayId(replayId.toString());
503514
}
515+
setOrgId(sentryOptions.getEffectiveOrgId());
504516
setSampleRate(sampleRate(samplingDecision));
505517
setSampled(StringUtils.toString(sampled(samplingDecision)));
506518
setSampleRand(sampleRand(samplingDecision));
@@ -536,6 +548,7 @@ public void setValuesFromScope(
536548
if (!SentryId.EMPTY_ID.equals(replayId)) {
537549
setReplayId(replayId.toString());
538550
}
551+
setOrgId(options.getEffectiveOrgId());
539552
setTransaction(null);
540553
setSampleRate(null);
541554
setSampled(null);
@@ -632,6 +645,7 @@ public static final class DSCKeys {
632645
public static final String SAMPLE_RAND = "sentry-sample_rand";
633646
public static final String SAMPLED = "sentry-sampled";
634647
public static final String REPLAY_ID = "sentry-replay_id";
648+
public static final String ORG_ID = "sentry-org_id";
635649

636650
public static final List<String> ALL =
637651
Arrays.asList(
@@ -644,6 +658,7 @@ public static final class DSCKeys {
644658
SAMPLE_RATE,
645659
SAMPLE_RAND,
646660
SAMPLED,
647-
REPLAY_ID);
661+
REPLAY_ID,
662+
ORG_ID);
648663
}
649664
}

sentry/src/main/java/io/sentry/Dsn.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
import io.sentry.util.Objects;
44
import java.net.URI;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
57
import org.jetbrains.annotations.NotNull;
68
import org.jetbrains.annotations.Nullable;
79

810
final class Dsn {
11+
private static final @NotNull Pattern ORG_ID_PATTERN = Pattern.compile("^o(\\d+)\\.");
12+
913
private final @NotNull String projectId;
1014
private final @Nullable String path;
1115
private final @Nullable String secretKey;
1216
private final @NotNull String publicKey;
1317
private final @NotNull URI sentryUri;
18+
private @Nullable String orgId;
1419

1520
/*
1621
/ The project ID which the authenticated user is bound to.
@@ -87,8 +92,25 @@ URI getSentryUri() {
8792
sentryUri =
8893
new URI(
8994
scheme, null, uri.getHost(), uri.getPort(), path + "api/" + projectId, null, null);
95+
96+
// Extract org ID from host (e.g., "o123.ingest.sentry.io" -> "123")
97+
final String host = uri.getHost();
98+
if (host != null) {
99+
final Matcher matcher = ORG_ID_PATTERN.matcher(host);
100+
if (matcher.find()) {
101+
orgId = matcher.group(1);
102+
}
103+
}
90104
} catch (Throwable e) {
91105
throw new IllegalArgumentException(e);
92106
}
93107
}
108+
109+
public @Nullable String getOrgId() {
110+
return orgId;
111+
}
112+
113+
void setOrgId(final @Nullable String orgId) {
114+
this.orgId = orgId;
115+
}
94116
}

sentry/src/main/java/io/sentry/PropagationContext.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,27 @@ public static PropagationContext fromHeaders(
2323
final @NotNull ILogger logger,
2424
final @Nullable String sentryTraceHeaderString,
2525
final @Nullable List<String> baggageHeaderStrings) {
26+
return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, null);
27+
}
28+
29+
public static @NotNull PropagationContext fromHeaders(
30+
final @NotNull ILogger logger,
31+
final @Nullable String sentryTraceHeaderString,
32+
final @Nullable List<String> baggageHeaderStrings,
33+
final @Nullable SentryOptions options) {
2634
if (sentryTraceHeaderString == null) {
2735
return new PropagationContext();
2836
}
2937

3038
try {
3139
final @NotNull SentryTraceHeader traceHeader = new SentryTraceHeader(sentryTraceHeaderString);
3240
final @NotNull Baggage baggage = Baggage.fromHeader(baggageHeaderStrings, logger);
41+
42+
if (options != null && !shouldContinueTrace(options, baggage)) {
43+
logger.log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch.");
44+
return new PropagationContext();
45+
}
46+
3347
return fromHeaders(traceHeader, baggage, null);
3448
} catch (InvalidSentryTraceHeaderException e) {
3549
logger.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
@@ -149,4 +163,25 @@ public void setSampled(final @Nullable Boolean sampled) {
149163
// should never be null since we ensure it in ctor
150164
return sampleRand == null ? 0.0 : sampleRand;
151165
}
166+
167+
static boolean shouldContinueTrace(
168+
final @NotNull SentryOptions options, final @Nullable Baggage baggage) {
169+
final @Nullable String sdkOrgId = options.getEffectiveOrgId();
170+
final @Nullable String baggageOrgId = baggage != null ? baggage.getOrgId() : null;
171+
172+
// Mismatched org IDs always reject regardless of strict mode
173+
if (sdkOrgId != null && baggageOrgId != null && !sdkOrgId.equals(baggageOrgId)) {
174+
return false;
175+
}
176+
177+
// In strict mode, both must be present and match (unless both are missing)
178+
if (options.isStrictTraceContinuation()) {
179+
if (sdkOrgId == null && baggageOrgId == null) {
180+
return true;
181+
}
182+
return sdkOrgId != null && sdkOrgId.equals(baggageOrgId);
183+
}
184+
185+
return true;
186+
}
152187
}

sentry/src/main/java/io/sentry/Scopes.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,8 @@ public void reportFullyDisplayed() {
11351135
final @Nullable String sentryTrace, final @Nullable List<String> baggageHeaders) {
11361136
@NotNull
11371137
PropagationContext propagationContext =
1138-
PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders);
1138+
PropagationContext.fromHeaders(
1139+
getOptions().getLogger(), sentryTrace, baggageHeaders, getOptions());
11391140
configureScope(
11401141
(scope) -> {
11411142
scope.withPropagationContext(

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,21 @@ public class SentryOptions {
427427
/** Whether to propagate W3C traceparent HTTP header. */
428428
private boolean propagateTraceparent = false;
429429

430+
/**
431+
* Controls whether the SDK requires matching org IDs from incoming baggage to continue a trace.
432+
* When true, both the SDK's org ID and the incoming baggage org ID must be present and match.
433+
* When false, a mismatch between present org IDs will still start a new trace, but missing org
434+
* IDs on either side are tolerated.
435+
*/
436+
private boolean strictTraceContinuation = false;
437+
438+
/**
439+
* An optional organization ID. The SDK will try to extract it from the DSN in most cases but you
440+
* can provide it explicitly for self-hosted and Relay setups. This value is used for trace
441+
* propagation and for features like {@link #strictTraceContinuation}.
442+
*/
443+
private @Nullable String orgId;
444+
430445
/** Proguard UUID. */
431446
private @Nullable String proguardUuid;
432447

@@ -2287,6 +2302,37 @@ public void setPropagateTraceparent(final boolean propagateTraceparent) {
22872302
this.propagateTraceparent = propagateTraceparent;
22882303
}
22892304

2305+
public boolean isStrictTraceContinuation() {
2306+
return strictTraceContinuation;
2307+
}
2308+
2309+
public void setStrictTraceContinuation(final boolean strictTraceContinuation) {
2310+
this.strictTraceContinuation = strictTraceContinuation;
2311+
}
2312+
2313+
public @Nullable String getOrgId() {
2314+
return orgId;
2315+
}
2316+
2317+
public void setOrgId(final @Nullable String orgId) {
2318+
this.orgId = orgId;
2319+
}
2320+
2321+
/**
2322+
* Returns the effective org ID, preferring the explicit config option over the DSN-parsed value.
2323+
*/
2324+
public @Nullable String getEffectiveOrgId() {
2325+
if (orgId != null) {
2326+
return orgId;
2327+
}
2328+
try {
2329+
final @Nullable String dsnOrgId = retrieveParsedDsn().getOrgId();
2330+
return dsnOrgId;
2331+
} catch (Throwable e) {
2332+
return null;
2333+
}
2334+
}
2335+
22902336
/**
22912337
* Returns a Proguard UUID.
22922338
*

sentry/src/test/java/io/sentry/DsnTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,36 @@ class DsnTest {
121121
Dsn("HTTP://publicKey:secretKey@host/path/id")
122122
Dsn("HTTPS://publicKey:secretKey@host/path/id")
123123
}
124+
125+
@Test
126+
fun `extracts org id from host`() {
127+
val dsn = Dsn("https://key@o123.ingest.sentry.io/456")
128+
assertEquals("123", dsn.orgId)
129+
}
130+
131+
@Test
132+
fun `extracts single digit org id from host`() {
133+
val dsn = Dsn("https://key@o1.ingest.us.sentry.io/456")
134+
assertEquals("1", dsn.orgId)
135+
}
136+
137+
@Test
138+
fun `returns null org id when host has no org prefix`() {
139+
val dsn = Dsn("https://key@sentry.io/456")
140+
assertNull(dsn.orgId)
141+
}
142+
143+
@Test
144+
fun `returns null org id for non-standard host`() {
145+
val dsn = Dsn("http://key@localhost:9000/456")
146+
assertNull(dsn.orgId)
147+
}
148+
149+
@Test
150+
fun `org id can be overridden via setter`() {
151+
val dsn = Dsn("https://key@o123.ingest.sentry.io/456")
152+
assertEquals("123", dsn.orgId)
153+
dsn.setOrgId("999")
154+
assertEquals("999", dsn.orgId)
155+
}
124156
}

0 commit comments

Comments
 (0)