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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Unreleased

### Features

- Add strict trace continuation support ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
- The SDK now extracts `org_id` from the DSN host and propagates it via `sentry-org_id` in the baggage header.
- When an incoming trace has a mismatched `org_id`, the SDK starts a new trace instead of continuing the foreign one.
- New option `strictTraceContinuation` (default `false`): when enabled, both the SDK's org ID and the incoming baggage org ID must be present and match for a trace to be continued.
- New option `orgId`: allows explicitly setting the organization ID for self-hosted and Relay setups where it cannot be extracted from the DSN.

## 8.34.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ final class ManifestMetadataReader {

static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";

static final String STRICT_TRACE_CONTINUATION = "io.sentry.strict-trace-continuation";
static final String ORG_ID = "io.sentry.org-id";

static final String SPOTLIGHT_ENABLE = "io.sentry.spotlight.enable";

static final String SPOTLIGHT_CONNECTION_URL = "io.sentry.spotlight.url";
Expand Down Expand Up @@ -658,6 +661,15 @@ static void applyMetadata(
feedbackOptions.setShowBranding(
readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding()));

options.setStrictTraceContinuation(
readBool(
metadata, logger, STRICT_TRACE_CONTINUATION, options.isStrictTraceContinuation()));

final @Nullable String orgId = readString(metadata, logger, ORG_ID, null);
if (orgId != null) {
options.setOrgId(orgId);
}

options.setEnableSpotlight(
readBool(metadata, logger, SPOTLIGHT_ENABLE, options.isEnableSpotlight()));

Expand Down
13 changes: 13 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 @@ -501,6 +504,7 @@ public final class io/sentry/ExternalOptions {
public fun getInAppExcludes ()Ljava/util/List;
public fun getInAppIncludes ()Ljava/util/List;
public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize;
public fun getOrgId ()Ljava/lang/String;
public fun getPrintUncaughtStackTrace ()Ljava/lang/Boolean;
public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle;
public fun getProfileSessionSampleRate ()Ljava/lang/Double;
Expand Down Expand Up @@ -528,6 +532,7 @@ public final class io/sentry/ExternalOptions {
public fun isGlobalHubMode ()Ljava/lang/Boolean;
public fun isSendDefaultPii ()Ljava/lang/Boolean;
public fun isSendModules ()Ljava/lang/Boolean;
public fun isStrictTraceContinuation ()Ljava/lang/Boolean;
public fun setCaptureOpenTelemetryEvents (Ljava/lang/Boolean;)V
public fun setCron (Lio/sentry/SentryOptions$Cron;)V
public fun setDebug (Ljava/lang/Boolean;)V
Expand All @@ -550,6 +555,7 @@ public final class io/sentry/ExternalOptions {
public fun setIgnoredErrors (Ljava/util/List;)V
public fun setIgnoredTransactions (Ljava/util/List;)V
public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V
public fun setOrgId (Ljava/lang/String;)V
public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V
public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V
public fun setProfileSessionSampleRate (Ljava/lang/Double;)V
Expand All @@ -564,6 +570,7 @@ public final class io/sentry/ExternalOptions {
public fun setSendModules (Ljava/lang/Boolean;)V
public fun setServerName (Ljava/lang/String;)V
public fun setSpotlightConnectionUrl (Ljava/lang/String;)V
public fun setStrictTraceContinuation (Ljava/lang/Boolean;)V
public fun setTag (Ljava/lang/String;Ljava/lang/String;)V
public fun setTracesSampleRate (Ljava/lang/Double;)V
}
Expand Down Expand Up @@ -2267,6 +2274,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 @@ -3569,6 +3577,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 @@ -3609,6 +3618,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 @@ -3679,6 +3689,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 @@ -3762,6 +3773,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 @@ -3793,6 +3805,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);
}
}
24 changes: 24 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;

/*
/ The project ID which the authenticated user is bound to.
Expand Down Expand Up @@ -87,8 +92,27 @@ 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")
String extractedOrgId = null;
final String host = uri.getHost();
if (host != null) {
final Matcher matcher = ORG_ID_PATTERN.matcher(host);
if (matcher.find()) {
extractedOrgId = matcher.group(1);
}
}
orgId = extractedOrgId;
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}
}

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

public void setOrgId(final @Nullable String orgId) {
this.orgId = orgId;
}
}
23 changes: 23 additions & 0 deletions sentry/src/main/java/io/sentry/ExternalOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public final class ExternalOptions {
private @Nullable String profilingTracesDirPath;
private @Nullable ProfileLifecycle profileLifecycle;

private @Nullable Boolean strictTraceContinuation;
private @Nullable String orgId;

private @Nullable SentryOptions.Cron cron;

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -213,6 +216,10 @@ public final class ExternalOptions {
options.setCron(cron);
}

options.setStrictTraceContinuation(
propertiesProvider.getBooleanProperty("strict-trace-continuation"));
options.setOrgId(propertiesProvider.getProperty("org-id"));

options.setEnableSpotlight(propertiesProvider.getBooleanProperty("enable-spotlight"));
options.setSpotlightConnectionUrl(propertiesProvider.getProperty("spotlight-connection-url"));
options.setProfileSessionSampleRate(
Expand Down Expand Up @@ -589,6 +596,22 @@ public void setProfilingTracesDirPath(@Nullable String profilingTracesDirPath) {
this.profilingTracesDirPath = profilingTracesDirPath;
}

public @Nullable Boolean isStrictTraceContinuation() {
return strictTraceContinuation;
}

public void setStrictTraceContinuation(final @Nullable Boolean strictTraceContinuation) {
this.strictTraceContinuation = strictTraceContinuation;
}

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

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

public @Nullable ProfileLifecycle getProfileLifecycle() {
return profileLifecycle;
}
Expand Down
41 changes: 41 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,33 @@ public static PropagationContext fromHeaders(
final @NotNull ILogger logger,
final @Nullable String sentryTraceHeaderString,
final @Nullable List<String> baggageHeaderStrings) {
@Nullable SentryOptions options = null;
try {
options = Sentry.getCurrentScopes().getOptions();
} catch (Throwable ignored) {
// options may not be available if Sentry is not initialized
}
return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, options);
}

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 +169,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
Loading