Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5b475d1
Add scope based feature flags
adinauer Oct 16, 2025
74cc427
fix equals
adinauer Oct 16, 2025
fb42140
add serialization tests
adinauer Oct 17, 2025
045337a
Create new reference on buffer clone
adinauer Oct 17, 2025
c1061ca
add comment explaining the merge method
adinauer Oct 17, 2025
74acb10
optimize merge method
adinauer Oct 17, 2025
eeae5c6
make flag and result nullable params
adinauer Oct 17, 2025
21806f2
handle empty/noop buffer on merge and add tests for it
adinauer Oct 17, 2025
95550ec
optimize add method
adinauer Oct 21, 2025
865ad5d
format; fix test
adinauer Oct 21, 2025
fa81bf8
Add rules for feature flags and mention in overview_dev
adinauer Oct 21, 2025
a5c7aef
Fix duplicate check
adinauer Oct 21, 2025
0ab241e
Update sentry/src/main/java/io/sentry/SentryOptions.java
adinauer Oct 21, 2025
4b757b8
Merge branch 'main' into feat/feature-flags-on-scope
adinauer Oct 21, 2025
ab1a146
Format code
getsentry-bot Oct 21, 2025
d320dd6
Merge branch 'feat/feature-flags-on-scope' into feat/feature-flags-rules
adinauer Oct 22, 2025
196b3b4
Add feature flags to spans
adinauer Oct 23, 2025
9194849
move feature flag buffer to span context; add e2e test
adinauer Oct 23, 2025
a4b6ca3
e2e tests; fix POTel feature flags
adinauer Oct 24, 2025
4ea930e
Format code
getsentry-bot Oct 24, 2025
e4e48db
test feature flag serialization from SentryTracer
adinauer Oct 24, 2025
d064953
Format code
getsentry-bot Oct 24, 2025
96f057b
fix e2e test assertions; handle null data map
adinauer Oct 24, 2025
6e66e07
Format code
getsentry-bot Oct 24, 2025
39dbf57
changelog
adinauer Oct 24, 2025
1cbda53
Merge branch 'main' into feat/feature-flags-on-scope
adinauer Nov 5, 2025
15544cd
changelog
adinauer Nov 6, 2025
9ac80b5
nullable getFeatureFlags
adinauer Nov 6, 2025
839d878
Merge branch 'main' into feat/feature-flags-on-scope
adinauer Nov 6, 2025
6041a82
Merge branch 'feat/feature-flags-on-scope' into feat/feature-flags-rules
adinauer Nov 6, 2025
7d8ba60
Merge branch 'feat/feature-flags-rules' into feat/feature-flags-on-spans
adinauer Nov 6, 2025
7a1bc74
update changelog
adinauer Nov 6, 2025
36be3b2
address pr review feedback
adinauer Nov 11, 2025
d7e80f3
Format code
getsentry-bot Nov 11, 2025
db8e32e
Merge branch 'main' into feat/feature-flags-on-spans
adinauer Nov 11, 2025
f54a1d9
fix(otel): Copy active span on scope clone (#4878)
adinauer Nov 11, 2025
e10c410
Merge branch 'main' into feat/feature-flags-on-spans
adinauer Nov 11, 2025
f07c6d1
api
adinauer Nov 11, 2025
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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

### Features

- Add feature flags API ([#4812](https://github.com/getsentry/sentry-java/pull/4812))
- Add feature flags API ([#4812](https://github.com/getsentry/sentry-java/pull/4812)) and ([#4831](https://github.com/getsentry/sentry-java/pull/4831))
- You may now keep track of your feature flag evaluations and have them show up in Sentry.
- You may use top level API (`Sentry.addFeatureFlag("my-feature-flag", true);`) or `IScope` and `IScopes` API
- Top level API (`Sentry.addFeatureFlag("my-feature-flag", true);`) writes to scopes and the current span (if there is one)
- It is also possible to use API on `IScope`, `IScopes`, `ISpan` and `ITransaction` directly
- Feature flag evaluations tracked on scope(s) will be added to any errors reported to Sentry.
- The SDK keeps the latest 100 evaluations from scope(s), replacing old entries as new evaluations are added.
- The SDK keeps the latest 100 evaluations from scope(s), replacing old entries as new evaluations are added.
- For feature flag evaluations tracked on spans:
- Only 10 evaluations are tracked per span, existing flags are updated but new ones exceeding the limit are ignored
- Spans do not inherit evaluations from their parent

### Fixes

Expand All @@ -20,6 +24,7 @@
- Fix log count in client reports ([#4869](https://github.com/getsentry/sentry-java/pull/4869))
- Fix profilerId propagation ([#4833](https://github.com/getsentry/sentry-java/pull/4833))
- Fix profiling init for Spring and Spring Boot w Agent auto-init ([#4815](https://github.com/getsentry/sentry-java/pull/4815))
- Copy active span on scope clone ([#4878](https://github.com/getsentry/sentry-java/pull/4878))

### Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFact

public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper {
public fun <init> (Lio/opentelemetry/api/trace/Span;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V
public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V
public fun finish ()V
public fun finish (Lio/sentry/SpanStatus;)V
public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V
Expand Down Expand Up @@ -96,6 +97,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/

public final class io/sentry/opentelemetry/OtelTransactionSpanForwarder : io/sentry/ITransaction {
public fun <init> (Lio/sentry/opentelemetry/IOtelSpanWrapper;)V
public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V
public fun finish ()V
public fun finish (Lio/sentry/SpanStatus;)V
public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,9 @@ public void setContext(@Nullable String key, @Nullable Object context) {
public @Nullable Attributes getOpenTelemetrySpanAttributes() {
return delegate.getOpenTelemetrySpanAttributes();
}

@Override
public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {
delegate.addFeatureFlag(flag, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,9 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource nameSou
}
return name;
}

@Override
public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {
rootSpan.addFeatureFlag(flag, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public final class io/sentry/opentelemetry/OtelSpanUtils {

public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper {
public fun <init> (Lio/opentelemetry/sdk/trace/ReadWriteSpan;Lio/sentry/IScopes;Lio/sentry/SentryDate;Lio/sentry/TracesSamplingDecision;Lio/sentry/opentelemetry/IOtelSpanWrapper;Lio/sentry/SpanId;Lio/sentry/Baggage;)V
public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V
public fun finish ()V
public fun finish (Lio/sentry/SpanStatus;)V
public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,11 @@ public Map<String, MeasurementValue> getMeasurements() {
return scopes;
}

@Override
public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {
context.addFeatureFlag(flag, result);
}

@Override
public @NotNull Context storeInContext(Context context) {
final @Nullable ReadWriteSpan otelSpan = getSpan();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
import io.sentry.SpanStatus;
import io.sentry.TransactionContext;
import io.sentry.TransactionOptions;
import io.sentry.featureflags.IFeatureFlagBuffer;
import io.sentry.protocol.Contexts;
import io.sentry.protocol.FeatureFlag;
import io.sentry.protocol.FeatureFlags;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.TransactionNameSource;
import java.util.Arrays;
Expand Down Expand Up @@ -260,6 +263,16 @@ private void transferSpanDetails(
targetSpan.setData(entry.getKey(), entry.getValue());
}

final @NotNull SpanContext spanContext = sourceSpan.getSpanContext();
final @NotNull IFeatureFlagBuffer featureFlagBuffer = spanContext.getFeatureFlagBuffer();
final @Nullable FeatureFlags featureFlags = featureFlagBuffer.getFeatureFlags();
if (featureFlags != null) {
for (FeatureFlag featureFlag : featureFlags.getValues()) {
targetSpan.setData(
FeatureFlag.DATA_PREFIX + featureFlag.getFlag(), featureFlag.getResult());
}
}

final @NotNull Map<String, String> tags = sourceSpan.getTags();
for (Map.Entry<String, String> entry : tags.entrySet()) {
targetSpan.setTag(entry.getKey(), entry.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public PersonController(PersonService personService, Tracer tracer) {
@GetMapping("{id}")
@WithSpan("personSpanThroughOtelAnnotation")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("outer-feature-flag", true);
Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan();
try (final @NotNull Scope spanScope = span.makeCurrent()) {
Sentry.logger().warn("warn Sentry logging");
Sentry.logger().error("error Sentry logging");
Sentry.logger().info("hello %s %s", "there", "world!");
Sentry.addFeatureFlag("my-feature-flag", true);
Sentry.addFeatureFlag("inner-feature-flag", true);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.systemtest

import io.sentry.protocol.FeatureFlag
import io.sentry.systemtest.util.TestHelper
import kotlin.test.Test
import kotlin.test.assertEquals
Expand All @@ -22,17 +23,31 @@ class PersonSystemTest {

testHelper.ensureErrorReceived { event ->
event.message?.formatted == "Trying person with id=1" &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
testHelper.doesEventHaveFlag(event, "inner-feature-flag", true)
}

testHelper.ensureErrorReceived { event ->
testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
testHelper.doesEventHaveFlag(event, "inner-feature-flag", true)
}

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
testHelper.doesTransactionHave(transaction, op = "http.server") &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "personSpanThroughOtelAnnotation",
featureFlag = FeatureFlag("flag.evaluation.outer-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughOtelApi",
featureFlag = FeatureFlag("flag.evaluation.inner-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughSentryApi",
noFeatureFlags = true,
)
}

Thread.sleep(10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public PersonController(PersonService personService, Tracer tracer) {

@GetMapping("{id}")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("transaction-feature-flag", true);
Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan();
try (final @NotNull Scope spanScope = span.makeCurrent()) {
Sentry.logger().warn("warn Sentry logging");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.systemtest

import io.sentry.protocol.FeatureFlag
import io.sentry.systemtest.util.TestHelper
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -31,8 +32,17 @@ class PersonSystemTest {
}

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
testHelper.doesTransactionHave(
transaction,
op = "http.server",
featureFlag = FeatureFlag("flag.evaluation.transaction-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughOtelApi",
featureFlag = FeatureFlag("flag.evaluation.my-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(transaction, op = "spanCreatedThroughSentryApi")
}

Thread.sleep(10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public PersonController(PersonService personService) {

@GetMapping("{id}")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("transaction-feature-flag", true);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.systemtest

import io.sentry.protocol.FeatureFlag
import io.sentry.systemtest.util.TestHelper
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -31,7 +32,16 @@ class PersonSystemTest {
}

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionHaveOp(transaction, "http.server")
testHelper.doesTransactionHave(
transaction,
op = "http.server",
featureFlag = FeatureFlag("flag.evaluation.transaction-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughSentryApi",
featureFlag = FeatureFlag("flag.evaluation.my-feature-flag", true),
)
Copy link

Choose a reason for hiding this comment

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

Bug: Feature Flags Attach to Wrong Active Span.

The test expects transaction-feature-flag on the transaction and my-feature-flag on the child span, but both flags are added using Sentry.addFeatureFlag() which writes to both scope and the current active span. When transaction-feature-flag is added before starting the child span, the current span is the transaction (root span). When my-feature-flag is added inside the child span, the current span is still the parent (since startChild() doesn't automatically make the child current unless ScopeBindingMode.ON is set). Both flags should end up on the transaction, not split between transaction and child span.

Fix in Cursor Fix in Web

}

Thread.sleep(10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public PersonController(PersonService personService, Tracer tracer) {
@GetMapping("{id}")
@WithSpan("personSpanThroughOtelAnnotation")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("outer-feature-flag", true);
Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan();
try (final @NotNull Scope spanScope = span.makeCurrent()) {
Sentry.logger().warn("warn Sentry logging");
Sentry.logger().error("error Sentry logging");
Sentry.logger().info("hello %s %s", "there", "world!");
Sentry.addFeatureFlag("my-feature-flag", true);
Sentry.addFeatureFlag("inner-feature-flag", true);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.systemtest

import io.sentry.protocol.FeatureFlag
import io.sentry.systemtest.util.TestHelper
import kotlin.test.Test
import kotlin.test.assertEquals
Expand All @@ -22,17 +23,31 @@ class PersonSystemTest {

testHelper.ensureErrorReceived { event ->
event.message?.formatted == "Trying person with id=1" &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
testHelper.doesEventHaveFlag(event, "inner-feature-flag", true)
}

testHelper.ensureErrorReceived { event ->
testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
testHelper.doesEventHaveFlag(event, "inner-feature-flag", true)
}

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
testHelper.doesTransactionHave(transaction, op = "http.server") &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "personSpanThroughOtelAnnotation",
featureFlag = FeatureFlag("flag.evaluation.outer-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughOtelApi",
featureFlag = FeatureFlag("flag.evaluation.inner-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughSentryApi",
noFeatureFlags = true,
)
}

Thread.sleep(10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public PersonController(PersonService personService, Tracer tracer) {

@GetMapping("{id}")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("transaction-feature-flag", true);
Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan();
try (final @NotNull Scope spanScope = span.makeCurrent()) {
Sentry.logger().warn("warn Sentry logging");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.systemtest

import io.sentry.protocol.FeatureFlag
import io.sentry.systemtest.util.TestHelper
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -31,8 +32,17 @@ class PersonSystemTest {
}

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
testHelper.doesTransactionHave(
transaction,
op = "http.server",
featureFlag = FeatureFlag("flag.evaluation.transaction-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughOtelApi",
featureFlag = FeatureFlag("flag.evaluation.my-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(transaction, op = "spanCreatedThroughSentryApi")
}

Thread.sleep(10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public PersonController(PersonService personService) {

@GetMapping("{id}")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("transaction-feature-flag", true);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.systemtest

import io.sentry.protocol.FeatureFlag
import io.sentry.systemtest.util.TestHelper
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -31,7 +32,16 @@ class PersonSystemTest {
}

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionHaveOp(transaction, "http.server")
testHelper.doesTransactionHave(
transaction,
op = "http.server",
featureFlag = FeatureFlag("flag.evaluation.transaction-feature-flag", true),
) &&
testHelper.doesTransactionHaveSpanWith(
transaction,
op = "spanCreatedThroughSentryApi",
featureFlag = FeatureFlag("flag.evaluation.my-feature-flag", true),
)
}

Thread.sleep(10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public PersonController(PersonService personService, Tracer tracer) {
@GetMapping("{id}")
@WithSpan("personSpanThroughOtelAnnotation")
Person person(@PathVariable Long id) {
Sentry.addFeatureFlag("outer-feature-flag", true);
Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan();
try (final @NotNull Scope spanScope = span.makeCurrent()) {
Sentry.logger().warn("warn Sentry logging");
Sentry.logger().error("error Sentry logging");
Sentry.logger().info("hello %s %s", "there", "world!");
Sentry.addFeatureFlag("my-feature-flag", true);
Sentry.addFeatureFlag("inner-feature-flag", true);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Loading
Loading