Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
24 changes: 24 additions & 0 deletions .cursor/rules/feature_flags.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
alwaysApply: false
description: Feature Flags
---
# Java SDK Feature Flags

There is a scope based and a span based API for tracking feature flag evaluations.

## Scope Based API

The `addFeatureFlag` method can be used to track feature flag evaluations. It exists on `Sentry` static API as well as `IScopes` and `IScope`.

The `maxFeatureFlags` option controls how many flags are tracked per scope and also how many are sent to Sentry as part of events.
Scope based feature flags can also be disabled by setting the value to 0. Defaults to 100 feature flag evaluations.

Order of feature flag evaluations is important as we only keep track of the last {maxFeatureFlag} items.

When a feature flag evluation with the same name is added, the previous one is removed and the new one is stored so that it'll be dropped last.

When sending out an error event, feature flag buffers from all three scope types (global, isolation and current scope) are merged, chosing the newest {maxFeatureFlag} entries across all scope types. Feature flags are sent as part of the `flags` context.

## Span Based API

tbd
Copy link
Member

Choose a reason for hiding this comment

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

Let's remember to update it

9 changes: 9 additions & 0 deletions .cursor/rules/overview_dev.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
- Rate limiting, cache rotation
- Android vs JVM caching differences

- **`feature_flags`**: Use when working with:
- Feature flag tracking and evaluation
- `addFeatureFlag()`, `getFeatureFlags()` methods
- `FeatureFlagBuffer`, `FeatureFlag` protocol
- `maxFeatureFlags` option and buffer management
- Feature flag merging across scope types
- Scope-based vs span-based feature flag APIs

### Integration & Infrastructure
- **`opentelemetry`**: Use when working with:
- OpenTelemetry modules (`sentry-opentelemetry-*`)
Expand Down Expand Up @@ -63,3 +71,4 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
- new module/integration/sample → `new_module`
- Cache/offline/network → `offline`
- System test/e2e/sample → `e2e_tests`
- Feature flag/addFeatureFlag/flag evaluation → `feature_flags`
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### Features

- Add feature flags API ([#4812](https://github.com/getsentry/sentry-java/pull/4812))
- 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
- 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.

### Fixes

- Removed SentryExecutorService limit for delayed scheduled tasks ([#4846](https://github.com/getsentry/sentry-java/pull/4846))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public static void main(String[] args) throws InterruptedException {
// Only data added to the scope on `configureScope` above is included.
Sentry.captureMessage("Some warning!", SentryLevel.WARNING);

Sentry.addFeatureFlag("my-feature-flag", true);

// Sending exception:
Exception exception = new RuntimeException("Some error!");
Sentry.captureException(exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ConsoleApplicationSystemTest {
// Verify we received the RuntimeException
testHelper.ensureErrorReceived { event ->
event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } ==
true
true && testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
}

// Verify we received the detailed event with fingerprint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ public static void main(String[] args) throws InterruptedException {
// Only data added to the scope on `configureScope` above is included.
Sentry.captureMessage("Some warning!", SentryLevel.WARNING);

Sentry.addFeatureFlag("my-feature-flag", true);

// Sending exception:
Exception exception = new RuntimeException("Some error!");
Sentry.captureException(exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class ConsoleApplicationSystemTest {
// Verify we received the RuntimeException
testHelper.ensureErrorReceived { event ->
event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } ==
true
true && testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
}

// Verify we received the detailed event with fingerprint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.samples.jul;

import io.sentry.Sentry;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.LogManager;
Expand All @@ -22,6 +23,10 @@ public static void main(String[] args) throws Exception {
MDC.put("userId", UUID.randomUUID().toString());
MDC.put("requestId", UUID.randomUUID().toString());

Sentry.addFeatureFlag("my-feature-flag", true);

LOGGER.warning("important warning");

// logging arguments are converted to Sentry Event parameters
LOGGER.log(Level.INFO, "User has made a purchase of product: %d", 445);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class ConsoleApplicationSystemTest {
} != null
}

testHelper.ensureErrorReceived { event ->
event.message?.message == "important warning" &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
}

testHelper.ensureLogsReceived { logs, _ ->
testHelper.doesContainLogWithBody(logs, "User has made a purchase of product: 445") &&
testHelper.doesContainLogWithBody(logs, "Something went wrong")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.samples.log4j2;

import io.sentry.Sentry;
import java.util.UUID;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -19,6 +20,8 @@ public static void main(String[] args) {
// ThreadContext tag not listed in log4j2.xml
ThreadContext.put("context-tag", "context-tag-value");

Sentry.addFeatureFlag("my-feature-flag", true);

// logging arguments are converted to Sentry Event parameters
LOGGER.info("User has made a purchase of product: {}", 445);
// because minimumEventLevel is set to WARN this raises an event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ class ConsoleApplicationSystemTest {
event.level?.name == "ERROR"
}

testHelper.ensureErrorReceived { event ->
event.message?.message == "Important warning" &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
}

testHelper.ensureErrorReceived { event ->
event.breadcrumbs?.firstOrNull {
it.message == "Hello Sentry!" && it.level == SentryLevel.DEBUG
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.samples.logback;

import io.sentry.Sentry;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -17,6 +18,9 @@ public static void main(String[] args) {
// MDC tag not listed in logback.xml
MDC.put("context-tag", "context-tag-value");

Sentry.addFeatureFlag("my-feature-flag", true);
LOGGER.warn("important warning");

// logging arguments are converted to Sentry Event parameters
LOGGER.info("User has made a purchase of product: {}", 445);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class ConsoleApplicationSystemTest {
} != null
}

testHelper.ensureErrorReceived { event ->
event.message?.message == "important warning" &&
testHelper.doesEventHaveFlag(event, "my-feature-flag", true)
}

testHelper.ensureErrorReceived { event ->
event.breadcrumbs?.firstOrNull {
it.message == "User has made a purchase of product: 445" && it.level == SentryLevel.INFO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Person person(@PathVariable("id") Long id) {
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);
LOGGER.info("Loading person with id={}", id);
if (id > 10L) {
throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class PersonSystemTest {
restClient.getPerson(11L)
assertEquals(500, restClient.lastKnownStatusCode)

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionHaveOp(transaction, "http.server")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Person person(@PathVariable Long id) {
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);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Person person(@PathVariable Long id) {
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);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Person person(@PathVariable Long id) {
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);
LOGGER.info("Loading person with id={}", id);
throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionHaveOp(transaction, "http.server")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Person person(@PathVariable Long id) {
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);
LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading"));
throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionHaveOp(transaction, "http.server")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Person person(@PathVariable Long id) {
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);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Person person(@PathVariable Long id) {
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);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Person person(@PathVariable Long id) {
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);
LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading"));
throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionHaveOp(transaction, "http.server")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Person person(@PathVariable Long id) {
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);
ISpan currentSpan = Sentry.getSpan();
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ class PersonSystemTest {
restClient.getPerson(1L)
assertEquals(500, restClient.lastKnownStatusCode)

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

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

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
Expand Down
Loading
Loading