Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Add strict trace continuation support ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some description here for customers to explain what this means for them.
We should also mention the new options.

- 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))
- OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint.
- 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.
Expand Down
9 changes: 9 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public final class io/sentry/Baggage {
public static fun fromHeader (Ljava/util/List;ZLio/sentry/ILogger;)Lio/sentry/Baggage;
public fun get (Ljava/lang/String;)Ljava/lang/String;
public fun getEnvironment ()Ljava/lang/String;
public fun getOrgId ()Ljava/lang/String;
public fun getPublicKey ()Ljava/lang/String;
public fun getRelease ()Ljava/lang/String;
public fun getReplayId ()Ljava/lang/String;
Expand All @@ -62,6 +63,7 @@ public final class io/sentry/Baggage {
public fun isShouldFreeze ()Z
public fun set (Ljava/lang/String;Ljava/lang/String;)V
public fun setEnvironment (Ljava/lang/String;)V
public fun setOrgId (Ljava/lang/String;)V
public fun setPublicKey (Ljava/lang/String;)V
public fun setRelease (Ljava/lang/String;)V
public fun setReplayId (Ljava/lang/String;)V
Expand All @@ -81,6 +83,7 @@ public final class io/sentry/Baggage {
public final class io/sentry/Baggage$DSCKeys {
public static final field ALL Ljava/util/List;
public static final field ENVIRONMENT Ljava/lang/String;
public static final field ORG_ID Ljava/lang/String;
public static final field PUBLIC_KEY Ljava/lang/String;
public static final field RELEASE Ljava/lang/String;
public static final field REPLAY_ID Ljava/lang/String;
Expand Down Expand Up @@ -2230,6 +2233,7 @@ public final class io/sentry/PropagationContext {
public static fun fromExistingTrace (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/lang/String;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/util/List;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/util/List;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;Lio/sentry/SpanId;)Lio/sentry/PropagationContext;
public fun getBaggage ()Lio/sentry/Baggage;
public fun getParentSpanId ()Lio/sentry/SpanId;
Expand Down Expand Up @@ -3510,6 +3514,7 @@ public class io/sentry/SentryOptions {
public fun getDistribution ()Lio/sentry/SentryOptions$DistributionOptions;
public fun getDistributionController ()Lio/sentry/IDistributionApi;
public fun getDsn ()Ljava/lang/String;
public fun getEffectiveOrgId ()Ljava/lang/String;
public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache;
public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader;
public fun getEnvironment ()Ljava/lang/String;
Expand Down Expand Up @@ -3550,6 +3555,7 @@ public class io/sentry/SentryOptions {
public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback;
public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode;
public fun getOptionsObservers ()Ljava/util/List;
public fun getOrgId ()Ljava/lang/String;
public fun getOutboxPath ()Ljava/lang/String;
public fun getPerformanceCollectors ()Ljava/util/List;
public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle;
Expand Down Expand Up @@ -3621,6 +3627,7 @@ public class io/sentry/SentryOptions {
public fun isSendDefaultPii ()Z
public fun isSendModules ()Z
public fun isStartProfilerOnAppStart ()Z
public fun isStrictTraceContinuation ()Z
public fun isTraceOptionsRequests ()Z
public fun isTraceSampling ()Z
public fun isTracingEnabled ()Z
Expand Down Expand Up @@ -3704,6 +3711,7 @@ public class io/sentry/SentryOptions {
public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V
public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V
public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V
public fun setOrgId (Ljava/lang/String;)V
public fun setPrintUncaughtStackTrace (Z)V
public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V
public fun setProfileSessionSampleRate (Ljava/lang/Double;)V
Expand Down Expand Up @@ -3736,6 +3744,7 @@ public class io/sentry/SentryOptions {
public fun setSpotlightConnectionUrl (Ljava/lang/String;)V
public fun setSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)V
public fun setStartProfilerOnAppStart (Z)V
public fun setStrictTraceContinuation (Z)V
public fun setTag (Ljava/lang/String;Ljava/lang/String;)V
public fun setThreadChecker (Lio/sentry/util/thread/IThreadChecker;)V
public fun setTraceOptionsRequests (Z)V
Expand Down
17 changes: 16 additions & 1 deletion sentry/src/main/java/io/sentry/Baggage.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ public static Baggage fromEvent(
baggage.setPublicKey(options.retrieveParsedDsn().getPublicKey());
baggage.setRelease(event.getRelease());
baggage.setEnvironment(event.getEnvironment());
baggage.setOrgId(options.getEffectiveOrgId());
baggage.setTransaction(transaction);
// we don't persist sample rate
baggage.setSampleRate(null);
Expand Down Expand Up @@ -450,6 +451,16 @@ public void setReplayId(final @Nullable String replayId) {
set(DSCKeys.REPLAY_ID, replayId);
}

@ApiStatus.Internal
public @Nullable String getOrgId() {
return get(DSCKeys.ORG_ID);
}

@ApiStatus.Internal
public void setOrgId(final @Nullable String orgId) {
set(DSCKeys.ORG_ID, orgId);
}

/**
* Sets / updates a value, but only if the baggage is still mutable.
*
Expand Down Expand Up @@ -501,6 +512,7 @@ public void setValuesFromTransaction(
if (replayId != null && !SentryId.EMPTY_ID.equals(replayId)) {
setReplayId(replayId.toString());
}
setOrgId(sentryOptions.getEffectiveOrgId());
setSampleRate(sampleRate(samplingDecision));
setSampled(StringUtils.toString(sampled(samplingDecision)));
setSampleRand(sampleRand(samplingDecision));
Expand Down Expand Up @@ -536,6 +548,7 @@ public void setValuesFromScope(
if (!SentryId.EMPTY_ID.equals(replayId)) {
setReplayId(replayId.toString());
}
setOrgId(options.getEffectiveOrgId());
setTransaction(null);
setSampleRate(null);
setSampled(null);
Expand Down Expand Up @@ -632,6 +645,7 @@ public static final class DSCKeys {
public static final String SAMPLE_RAND = "sentry-sample_rand";
public static final String SAMPLED = "sentry-sampled";
public static final String REPLAY_ID = "sentry-replay_id";
public static final String ORG_ID = "sentry-org_id";

public static final List<String> ALL =
Arrays.asList(
Expand All @@ -644,6 +658,7 @@ public static final class DSCKeys {
SAMPLE_RATE,
SAMPLE_RAND,
SAMPLED,
REPLAY_ID);
REPLAY_ID,
ORG_ID);
}
}
22 changes: 22 additions & 0 deletions sentry/src/main/java/io/sentry/Dsn.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

import io.sentry.util.Objects;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

final class Dsn {
private static final @NotNull Pattern ORG_ID_PATTERN = Pattern.compile("^o(\\d+)\\.");

private final @NotNull String projectId;
private final @Nullable String path;
private final @Nullable String secretKey;
private final @NotNull String publicKey;
private final @NotNull URI sentryUri;
private @Nullable String orgId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be final


/*
/ The project ID which the authenticated user is bound to.
Expand Down Expand Up @@ -84,8 +89,25 @@ URI getSentryUri() {
sentryUri =
new URI(
scheme, null, uri.getHost(), uri.getPort(), path + "api/" + projectId, null, null);

// Extract org ID from host (e.g., "o123.ingest.sentry.io" -> "123")
final String host = uri.getHost();
if (host != null) {
final Matcher matcher = ORG_ID_PATTERN.matcher(host);
if (matcher.find()) {
orgId = matcher.group(1);
}
}
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}
}

public @Nullable String getOrgId() {
return orgId;
}

void setOrgId(final @Nullable String orgId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No setter needed here.

this.orgId = orgId;
}
}
35 changes: 35 additions & 0 deletions sentry/src/main/java/io/sentry/PropagationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,27 @@ public static PropagationContext fromHeaders(
final @NotNull ILogger logger,
final @Nullable String sentryTraceHeaderString,
final @Nullable List<String> baggageHeaderStrings) {
return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use Sentry.getCurrentScopes().getOptions() here to default to the current options.
The better fix would probably be to replace call sites with passing in options and remove this overload.
Since PropagationContext is marked internal, we're free to change / replace / remove methods.

}

public static @NotNull PropagationContext fromHeaders(
final @NotNull ILogger logger,
final @Nullable String sentryTraceHeaderString,
final @Nullable List<String> baggageHeaderStrings,
final @Nullable SentryOptions options) {
if (sentryTraceHeaderString == null) {
return new PropagationContext();
}

try {
final @NotNull SentryTraceHeader traceHeader = new SentryTraceHeader(sentryTraceHeaderString);
final @NotNull Baggage baggage = Baggage.fromHeader(baggageHeaderStrings, logger);

if (options != null && !shouldContinueTrace(options, baggage)) {
logger.log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch.");
return new PropagationContext();
}

return fromHeaders(traceHeader, baggage, null);
} catch (InvalidSentryTraceHeaderException e) {
logger.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
Expand Down Expand Up @@ -149,4 +163,25 @@ public void setSampled(final @Nullable Boolean sampled) {
// should never be null since we ensure it in ctor
return sampleRand == null ? 0.0 : sampleRand;
}

static boolean shouldContinueTrace(
final @NotNull SentryOptions options, final @Nullable Baggage baggage) {
final @Nullable String sdkOrgId = options.getEffectiveOrgId();
final @Nullable String baggageOrgId = baggage != null ? baggage.getOrgId() : null;

// Mismatched org IDs always reject regardless of strict mode
if (sdkOrgId != null && baggageOrgId != null && !sdkOrgId.equals(baggageOrgId)) {
return false;
}

// In strict mode, both must be present and match (unless both are missing)
if (options.isStrictTraceContinuation()) {
if (sdkOrgId == null && baggageOrgId == null) {
return true;
}
return sdkOrgId != null && sdkOrgId.equals(baggageOrgId);
}

return true;
}
}
3 changes: 2 additions & 1 deletion sentry/src/main/java/io/sentry/Scopes.java
Original file line number Diff line number Diff line change
Expand Up @@ -1135,7 +1135,8 @@ public void reportFullyDisplayed() {
final @Nullable String sentryTrace, final @Nullable List<String> baggageHeaders) {
@NotNull
PropagationContext propagationContext =
PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders);
PropagationContext.fromHeaders(
getOptions().getLogger(), sentryTrace, baggageHeaders, getOptions());
configureScope(
(scope) -> {
scope.withPropagationContext(
Expand Down
46 changes: 46 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,21 @@ public class SentryOptions {
/** Whether to propagate W3C traceparent HTTP header. */
private boolean propagateTraceparent = false;

/**
* Controls whether the SDK requires matching org IDs from incoming baggage to continue a trace.
* When true, both the SDK's org ID and the incoming baggage org ID must be present and match.
* When false, a mismatch between present org IDs will still start a new trace, but missing org
* IDs on either side are tolerated.
*/
private boolean strictTraceContinuation = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add both of these options to ExternalOptions and ManifestMetadataReader as well.
There's no tests for options.
We have https://github.com/getsentry/sentry-java/blob/main/.cursor/rules/options.mdc which explains details around options in this SDK.


/**
* An optional organization ID. The SDK will try to extract it from the DSN in most cases but you
* can provide it explicitly for self-hosted and Relay setups. This value is used for trace
* propagation and for features like {@link #strictTraceContinuation}.
*/
private @Nullable String orgId;

/** Proguard UUID. */
private @Nullable String proguardUuid;

Expand Down Expand Up @@ -2287,6 +2302,37 @@ public void setPropagateTraceparent(final boolean propagateTraceparent) {
this.propagateTraceparent = propagateTraceparent;
}

public boolean isStrictTraceContinuation() {
return strictTraceContinuation;
}

public void setStrictTraceContinuation(final boolean strictTraceContinuation) {
this.strictTraceContinuation = strictTraceContinuation;
}

public @Nullable String getOrgId() {
return orgId;
}

public void setOrgId(final @Nullable String orgId) {
this.orgId = orgId;
}

/**
* Returns the effective org ID, preferring the explicit config option over the DSN-parsed value.
*/
public @Nullable String getEffectiveOrgId() {
if (orgId != null) {
return orgId;
}
try {
final @Nullable String dsnOrgId = retrieveParsedDsn().getOrgId();
return dsnOrgId;
} catch (Throwable e) {
return null;
}
}

/**
* Returns a Proguard UUID.
*
Expand Down
32 changes: 32 additions & 0 deletions sentry/src/test/java/io/sentry/DsnTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,36 @@ class DsnTest {
Dsn("HTTP://publicKey:secretKey@host/path/id")
Dsn("HTTPS://publicKey:secretKey@host/path/id")
}

@Test
fun `extracts org id from host`() {
val dsn = Dsn("https://key@o123.ingest.sentry.io/456")
assertEquals("123", dsn.orgId)
}

@Test
fun `extracts single digit org id from host`() {
val dsn = Dsn("https://key@o1.ingest.us.sentry.io/456")
assertEquals("1", dsn.orgId)
}

@Test
fun `returns null org id when host has no org prefix`() {
val dsn = Dsn("https://key@sentry.io/456")
assertNull(dsn.orgId)
}

@Test
fun `returns null org id for non-standard host`() {
val dsn = Dsn("http://key@localhost:9000/456")
assertNull(dsn.orgId)
}

@Test
fun `org id can be overridden via setter`() {
val dsn = Dsn("https://key@o123.ingest.sentry.io/456")
assertEquals("123", dsn.orgId)
dsn.setOrgId("999")
assertEquals("999", dsn.orgId)
}
}
Loading
Loading