From 5cab647434bdc7ebd4da66b9c58354b98ddeb3ab Mon Sep 17 00:00:00 2001 From: richard elms Date: Fri, 5 Dec 2025 10:56:38 +0100 Subject: [PATCH 1/6] initial agent work --- .../src/main/java/com/bugsnag/Bugsnag.java | 66 +++++- .../main/java/com/bugsnag/Configuration.java | 56 +++++ .../main/java/com/bugsnag/FeatureFlag.java | 80 +++++++ .../java/com/bugsnag/FeatureFlagStore.java | 116 ++++++++++ bugsnag/src/main/java/com/bugsnag/Report.java | 82 ++++++- .../com/bugsnag/BugsnagFeatureFlagTest.java | 152 +++++++++++++ .../bugsnag/ConfigurationFeatureFlagTest.java | 118 ++++++++++ .../com/bugsnag/FeatureFlagStoreTest.java | 187 ++++++++++++++++ .../java/com/bugsnag/FeatureFlagTest.java | 74 +++++++ .../com/bugsnag/ReportFeatureFlagTest.java | 208 ++++++++++++++++++ features/feature_flags.feature | 72 ++++++ .../scenarios/ClearFeatureFlagScenario.java | 21 ++ .../FeatureFlagCallbackScenario.java | 21 ++ .../FeatureFlagOverrideScenario.java | 25 +++ .../scenarios/FeatureFlagScenario.java | 19 ++ .../MultipleFeatureFlagsScenario.java | 21 ++ 16 files changed, 1314 insertions(+), 4 deletions(-) create mode 100644 bugsnag/src/main/java/com/bugsnag/FeatureFlag.java create mode 100644 bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java create mode 100644 bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java create mode 100644 bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java create mode 100644 bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java create mode 100644 bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java create mode 100644 bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java create mode 100644 features/feature_flags.feature create mode 100644 features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ClearFeatureFlagScenario.java create mode 100644 features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagCallbackScenario.java create mode 100644 features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagOverrideScenario.java create mode 100644 features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagScenario.java create mode 100644 features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleFeatureFlagsScenario.java diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 8914ac15..55d4ab7c 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -11,6 +11,7 @@ import java.io.Closeable; import java.lang.Thread.UncaughtExceptionHandler; import java.net.Proxy; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Set; @@ -57,6 +58,7 @@ public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) { private Configuration config; private final SessionTracker sessionTracker; + private final FeatureFlagStore featureFlagStore; private static final ThreadLocal THREAD_METADATA = new ThreadLocal() { @Override @@ -91,6 +93,7 @@ public Bugsnag(String apiKey, boolean sendUncaughtExceptions) { config = new Configuration(apiKey); sessionTracker = new SessionTracker(config); + featureFlagStore = config.copyFeatureFlagStore(); // Automatically send unhandled exceptions to Bugsnag using this Bugsnag config.setSendUncaughtExceptions(sendUncaughtExceptions); @@ -348,7 +351,9 @@ public void setTimeout(int timeout) { * @see #notify(com.bugsnag.Report) */ public Report buildReport(Throwable throwable) { - return new Report(config, throwable); + HandledState handledState = HandledState.newInstance( + HandledState.SeverityReasonType.REASON_HANDLED_EXCEPTION); + return new Report(config, throwable, handledState, Thread.currentThread(), featureFlagStore); } /** @@ -404,7 +409,7 @@ public boolean notify(Throwable throwable, Severity severity, Callback callback) HandledState handledState = HandledState.newInstance( HandledState.SeverityReasonType.REASON_USER_SPECIFIED, severity); - Report report = new Report(config, throwable, handledState, Thread.currentThread()); + Report report = new Report(config, throwable, handledState, Thread.currentThread(), featureFlagStore); return notify(report, callback); } @@ -422,7 +427,7 @@ public boolean notify(Report report) { } boolean notify(Throwable throwable, HandledState handledState, Thread currentThread) { - Report report = new Report(config, throwable, handledState, currentThread); + Report report = new Report(config, throwable, handledState, currentThread, featureFlagStore); return notify(report, null); } @@ -679,4 +684,59 @@ public static Set uncaughtExceptionClients() { void addOnSession(OnSession onSession) { sessionTracker.addOnSession(onSession); } + + /** + * Add a feature flag with the specified name and variant. + * If the name already exists, the variant will be updated. + * + * @param name the feature flag name + * @param variant the feature flag variant (can be null) + */ + public void addFeatureFlag(String name, String variant) { + featureFlagStore.addFeatureFlag(name, variant); + } + + /** + * Add a feature flag with the specified name and no variant. + * + * @param name the feature flag name + */ + public void addFeatureFlag(String name) { + addFeatureFlag(name, null); + } + + /** + * Add multiple feature flags. + * If any names already exist, their variants will be updated. + * + * @param featureFlags the feature flags to add + */ + public void addFeatureFlags(Collection featureFlags) { + featureFlagStore.addFeatureFlags(featureFlags); + } + + /** + * Remove the feature flag with the specified name. + * + * @param name the feature flag name to remove + */ + public void clearFeatureFlag(String name) { + featureFlagStore.clearFeatureFlag(name); + } + + /** + * Remove all feature flags. + */ + public void clearFeatureFlags() { + featureFlagStore.clearFeatureFlags(); + } + + /** + * Get a copy of the feature flag store. + * + * @return a copy of the feature flag store + */ + FeatureFlagStore copyFeatureFlagStore() { + return featureFlagStore.copy(); + } } diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index 7fd5d536..8cccd37d 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -48,6 +48,7 @@ public class Configuration { Collection callbacks = new ConcurrentLinkedQueue(); private final AtomicBoolean autoCaptureSessions = new AtomicBoolean(true); private final AtomicBoolean sendUncaughtExceptions = new AtomicBoolean(true); + private final FeatureFlagStore featureFlagStore = new FeatureFlagStore(); Configuration(String apiKey) { this.apiKey = apiKey; @@ -299,4 +300,59 @@ public Serializer getSerializer() { public void setSerializer(Serializer serializer) { this.serializer = serializer; } + + /** + * Add a feature flag with the specified name and variant. + * If the name already exists, the variant will be updated. + * + * @param name the feature flag name + * @param variant the feature flag variant (can be null) + */ + public void addFeatureFlag(String name, String variant) { + featureFlagStore.addFeatureFlag(name, variant); + } + + /** + * Add a feature flag with the specified name and no variant. + * + * @param name the feature flag name + */ + public void addFeatureFlag(String name) { + addFeatureFlag(name, null); + } + + /** + * Add multiple feature flags. + * If any names already exist, their variants will be updated. + * + * @param featureFlags the feature flags to add + */ + public void addFeatureFlags(Collection featureFlags) { + featureFlagStore.addFeatureFlags(featureFlags); + } + + /** + * Remove the feature flag with the specified name. + * + * @param name the feature flag name to remove + */ + public void clearFeatureFlag(String name) { + featureFlagStore.clearFeatureFlag(name); + } + + /** + * Remove all feature flags. + */ + public void clearFeatureFlags() { + featureFlagStore.clearFeatureFlags(); + } + + /** + * Get a copy of the feature flag store. + * + * @return a copy of the feature flag store + */ + FeatureFlagStore copyFeatureFlagStore() { + return featureFlagStore.copy(); + } } diff --git a/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java b/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java new file mode 100644 index 00000000..0df5b633 --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java @@ -0,0 +1,80 @@ +package com.bugsnag; + +import com.bugsnag.serialization.Expose; + +import java.util.Objects; + +/** + * Represents a feature flag with a name and optional variant. + * Feature flags can be used to annotate events with information about + * active experiments or A/B tests. + */ +public class FeatureFlag { + private final String name; + private final String variant; + + /** + * Create a feature flag with a name and no variant. + * + * @param name the name of the feature flag + */ + public FeatureFlag(String name) { + this(name, null); + } + + /** + * Create a feature flag with a name and variant. + * + * @param name the name of the feature flag + * @param variant the variant of the feature flag (can be null) + */ + public FeatureFlag(String name, String variant) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Feature flag name cannot be null or empty"); + } + this.name = name; + this.variant = variant; + } + + /** + * Get the name of the feature flag. + * + * @return the feature flag name + */ + @Expose + public String getName() { + return name; + } + + /** + * Get the variant of the feature flag. + * + * @return the feature flag variant, or null if not set + */ + @Expose + public String getVariant() { + return variant; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + FeatureFlag that = (FeatureFlag) obj; + return Objects.equals(name, that.name) && Objects.equals(variant, that.variant); + } + + @Override + public int hashCode() { + return Objects.hash(name, variant); + } + + @Override + public String toString() { + return "FeatureFlag{name='" + name + "', variant='" + variant + "'}"; + } +} diff --git a/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java b/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java new file mode 100644 index 00000000..ffd52c30 --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java @@ -0,0 +1,116 @@ +package com.bugsnag; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Internal storage for feature flags that maintains insertion order. + * This class is thread-safe for concurrent access. + */ +class FeatureFlagStore { + // LinkedHashMap maintains insertion order + private final Map flags = new LinkedHashMap(); + + /** + * Add a feature flag with the specified name and variant. + * If the name already exists, the variant will be updated without changing position. + * + * @param name the feature flag name + * @param variant the feature flag variant (can be null) + */ + synchronized void addFeatureFlag(String name, String variant) { + if (name == null || name.isEmpty()) { + return; + } + flags.put(name, variant); + } + + /** + * Add multiple feature flags. + * If any names already exist, their variants will be updated without changing position. + * + * @param featureFlags the feature flags to add + */ + synchronized void addFeatureFlags(Collection featureFlags) { + if (featureFlags == null) { + return; + } + for (FeatureFlag flag : featureFlags) { + if (flag != null) { + addFeatureFlag(flag.getName(), flag.getVariant()); + } + } + } + + /** + * Remove the feature flag with the specified name. + * + * @param name the feature flag name to remove + */ + synchronized void clearFeatureFlag(String name) { + if (name != null) { + flags.remove(name); + } + } + + /** + * Remove all feature flags. + */ + synchronized void clearFeatureFlags() { + flags.clear(); + } + + /** + * Get a list of all feature flags in insertion order. + * + * @return an unmodifiable list of feature flags + */ + synchronized List toList() { + List result = new ArrayList(flags.size()); + for (Map.Entry entry : flags.entrySet()) { + result.add(new FeatureFlag(entry.getKey(), entry.getValue())); + } + return result; + } + + /** + * Create a copy of this store with all the same flags. + * + * @return a new FeatureFlagStore with the same flags + */ + synchronized FeatureFlagStore copy() { + FeatureFlagStore copy = new FeatureFlagStore(); + copy.flags.putAll(this.flags); + return copy; + } + + /** + * Merge flags from another store into this one. + * Flags from the other store will overwrite existing flags with the same name, + * but will not change the position of existing flags. + * + * @param other the other store to merge from + */ + synchronized void merge(FeatureFlagStore other) { + if (other == null) { + return; + } + synchronized (other) { + for (Map.Entry entry : other.flags.entrySet()) { + flags.put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Get the number of feature flags. + * + * @return the number of feature flags + */ + synchronized int size() { + return flags.size(); + } +} diff --git a/bugsnag/src/main/java/com/bugsnag/Report.java b/bugsnag/src/main/java/com/bugsnag/Report.java index 01899032..bf176551 100644 --- a/bugsnag/src/main/java/com/bugsnag/Report.java +++ b/bugsnag/src/main/java/com/bugsnag/Report.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ public class Report { private boolean shouldCancel = false; private Map sessionMap; private final List threadStates; + private final FeatureFlagStore featureFlagStore; /** * Create a report for the error. @@ -34,16 +36,27 @@ public class Report { */ protected Report(Configuration config, Throwable throwable) { this(config, throwable, HandledState.newInstance( - HandledState.SeverityReasonType.REASON_HANDLED_EXCEPTION), Thread.currentThread()); + HandledState.SeverityReasonType.REASON_HANDLED_EXCEPTION), Thread.currentThread(), null); } Report(Configuration config, Throwable throwable, HandledState handledState, Thread currentThread) { + this(config, throwable, handledState, currentThread, null); + } + + Report(Configuration config, Throwable throwable, + HandledState handledState, Thread currentThread, FeatureFlagStore clientFeatureFlagStore) { this.config = config; this.exception = new Exception(config, throwable); this.handledState = handledState; this.severity = handledState.getOriginalSeverity(); diagnostics = new Diagnostics(this.config); + + // Initialize feature flags: start with config, then merge client flags + featureFlagStore = config.copyFeatureFlagStore(); + if (clientFeatureFlagStore != null) { + featureFlagStore.merge(clientFeatureFlagStore); + } if (config.isSendThreads()) { Throwable exc = handledState.isUnhandled() ? throwable : null; @@ -340,6 +353,73 @@ void mergeMetadata(Metadata metadata) { diagnostics.metadata.merge(metadata); } + /** + * Get the list of feature flags for this report. + * The order reflects when flags were first added across Configuration, Client, and Event scopes. + * + * @return an unmodifiable list of feature flags + */ + @Expose + public List getFeatureFlags() { + return featureFlagStore.toList(); + } + + /** + * Add a feature flag with the specified name and variant. + * If the name already exists, the variant will be updated. + * + * @param name the feature flag name + * @param variant the feature flag variant (can be null) + * @return the modified report + */ + public Report addFeatureFlag(String name, String variant) { + featureFlagStore.addFeatureFlag(name, variant); + return this; + } + + /** + * Add a feature flag with the specified name and no variant. + * + * @param name the feature flag name + * @return the modified report + */ + public Report addFeatureFlag(String name) { + return addFeatureFlag(name, null); + } + + /** + * Add multiple feature flags. + * If any names already exist, their variants will be updated. + * + * @param featureFlags the feature flags to add + * @return the modified report + */ + public Report addFeatureFlags(Collection featureFlags) { + featureFlagStore.addFeatureFlags(featureFlags); + return this; + } + + /** + * Remove the feature flag with the specified name. + * + * @param name the feature flag name to remove + * @return the modified report + */ + public Report clearFeatureFlag(String name) { + featureFlagStore.clearFeatureFlag(name); + return this; + } + + /** + * Remove all feature flags. + * + * @return the modified report + */ + public Report clearFeatureFlags() { + featureFlagStore.clearFeatureFlags(); + return this; + } + static class SeverityReason { private final String type; private final Map attributes; diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java new file mode 100644 index 00000000..4563dc81 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java @@ -0,0 +1,152 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for feature flags in Bugsnag client + */ +public class BugsnagFeatureFlagTest { + + private Bugsnag bugsnag; + + @Before + public void setUp() { + bugsnag = new Bugsnag("api-key", false); + } + + @After + public void tearDown() { + bugsnag.close(); + } + + @Test + public void testAddFeatureFlag() { + bugsnag.addFeatureFlag("flag1", "variant-a"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("variant-a", flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlagWithoutVariant() { + bugsnag.addFeatureFlag("flag1"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals(null, flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlags() { + List flagsToAdd = new ArrayList(); + flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); + flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + + bugsnag.addFeatureFlags(flagsToAdd); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(2, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + } + + @Test + public void testClearFeatureFlag() { + bugsnag.addFeatureFlag("flag1", "variant-a"); + bugsnag.addFeatureFlag("flag2", "variant-b"); + bugsnag.clearFeatureFlag("flag1"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + } + + @Test + public void testClearFeatureFlags() { + bugsnag.addFeatureFlag("flag1", "variant-a"); + bugsnag.addFeatureFlag("flag2", "variant-b"); + bugsnag.clearFeatureFlags(); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(0, flags.size()); + } + + @Test + public void testClientFlagsInheritFromConfiguration() { + Configuration config = bugsnag.getConfig(); + config.addFeatureFlag("config-flag", "config-variant"); + + Bugsnag client = new Bugsnag("api-key", false); + client.getConfig().addFeatureFlag("config-flag", "config-variant"); + + Report report = client.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("config-flag", flags.get(0).getName()); + assertEquals("config-variant", flags.get(0).getVariant()); + + client.close(); + } + + @Test + public void testClientFlagsOverrideConfigurationFlags() { + Configuration config = bugsnag.getConfig(); + config.addFeatureFlag("flag1", "config-variant"); + + Bugsnag client = new Bugsnag("api-key", false); + client.getConfig().addFeatureFlag("flag1", "config-variant"); + client.addFeatureFlag("flag1", "client-variant"); + + Report report = client.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("client-variant", flags.get(0).getVariant()); + + client.close(); + } + + @Test + public void testFeatureFlagOrderPreservedAcrossScopes() { + Configuration config = bugsnag.getConfig(); + config.addFeatureFlag("flag1", "config-variant"); + config.addFeatureFlag("flag2", "config-variant"); + + Bugsnag client = new Bugsnag("api-key", false); + client.getConfig().addFeatureFlag("flag1", "config-variant"); + client.getConfig().addFeatureFlag("flag2", "config-variant"); + client.addFeatureFlag("flag3", "client-variant"); + + Report report = client.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(3, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + assertEquals("flag3", flags.get(2).getName()); + + client.close(); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java new file mode 100644 index 00000000..d079315f --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java @@ -0,0 +1,118 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for feature flags in Configuration + */ +public class ConfigurationFeatureFlagTest { + + private Configuration config; + + @Before + public void setUp() { + config = new Configuration("api-key"); + } + + @Test + public void testAddFeatureFlag() { + config.addFeatureFlag("flag1", "variant-a"); + + // Verify by creating a client and checking the flags are inherited + Bugsnag bugsnag = new Bugsnag("api-key"); + bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("variant-a", flags.get(0).getVariant()); + + bugsnag.close(); + } + + @Test + public void testAddFeatureFlagWithoutVariant() { + config.addFeatureFlag("flag1"); + + Bugsnag bugsnag = new Bugsnag("api-key"); + bugsnag.getConfig().addFeatureFlag("flag1"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals(null, flags.get(0).getVariant()); + + bugsnag.close(); + } + + @Test + public void testAddFeatureFlags() { + List flagsToAdd = new ArrayList(); + flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); + flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + + config.addFeatureFlags(flagsToAdd); + + Bugsnag bugsnag = new Bugsnag("api-key"); + bugsnag.getConfig().addFeatureFlags(flagsToAdd); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(2, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + + bugsnag.close(); + } + + @Test + public void testClearFeatureFlag() { + config.addFeatureFlag("flag1", "variant-a"); + config.addFeatureFlag("flag2", "variant-b"); + config.clearFeatureFlag("flag1"); + + Bugsnag bugsnag = new Bugsnag("api-key"); + bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); + bugsnag.getConfig().addFeatureFlag("flag2", "variant-b"); + bugsnag.getConfig().clearFeatureFlag("flag1"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + + bugsnag.close(); + } + + @Test + public void testClearFeatureFlags() { + config.addFeatureFlag("flag1", "variant-a"); + config.addFeatureFlag("flag2", "variant-b"); + config.clearFeatureFlags(); + + Bugsnag bugsnag = new Bugsnag("api-key"); + bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); + bugsnag.getConfig().addFeatureFlag("flag2", "variant-b"); + bugsnag.getConfig().clearFeatureFlags(); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(0, flags.size()); + + bugsnag.close(); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java new file mode 100644 index 00000000..75905f70 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java @@ -0,0 +1,187 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for FeatureFlagStore + */ +public class FeatureFlagStoreTest { + + private FeatureFlagStore store; + + @Before + public void setUp() { + store = new FeatureFlagStore(); + } + + @Test + public void testAddFeatureFlag() { + store.addFeatureFlag("flag1", "variant-a"); + List flags = store.toList(); + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("variant-a", flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlagWithNullVariant() { + store.addFeatureFlag("flag1", null); + List flags = store.toList(); + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals(null, flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlagUpdatesVariant() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag1", "variant-b"); + + List flags = store.toList(); + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("variant-b", flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlagMaintainsOrder() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + store.addFeatureFlag("flag3", "variant-c"); + + List flags = store.toList(); + assertEquals(3, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + assertEquals("flag3", flags.get(2).getName()); + } + + @Test + public void testAddFeatureFlagUpdateDoesNotChangeOrder() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + store.addFeatureFlag("flag1", "variant-updated"); + + List flags = store.toList(); + assertEquals(2, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("variant-updated", flags.get(0).getVariant()); + assertEquals("flag2", flags.get(1).getName()); + } + + @Test + public void testAddFeatureFlags() { + List flagsToAdd = new ArrayList(); + flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); + flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + + store.addFeatureFlags(flagsToAdd); + + List flags = store.toList(); + assertEquals(2, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + } + + @Test + public void testClearFeatureFlag() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + + store.clearFeatureFlag("flag1"); + + List flags = store.toList(); + assertEquals(1, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + } + + @Test + public void testClearFeatureFlagAndReAdd() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + + store.clearFeatureFlag("flag1"); + store.addFeatureFlag("flag1", "variant-updated"); + + List flags = store.toList(); + assertEquals(2, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + assertEquals("flag1", flags.get(1).getName()); + } + + @Test + public void testClearFeatureFlags() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + + store.clearFeatureFlags(); + + List flags = store.toList(); + assertEquals(0, flags.size()); + } + + @Test + public void testCopy() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + + FeatureFlagStore copy = store.copy(); + + List originalFlags = store.toList(); + List copiedFlags = copy.toList(); + + assertEquals(originalFlags.size(), copiedFlags.size()); + assertEquals("flag1", copiedFlags.get(0).getName()); + assertEquals("flag2", copiedFlags.get(1).getName()); + + // Verify that modifying the copy doesn't affect the original + copy.addFeatureFlag("flag3", "variant-c"); + assertEquals(2, store.toList().size()); + assertEquals(3, copy.toList().size()); + } + + @Test + public void testMerge() { + store.addFeatureFlag("flag1", "variant-a"); + store.addFeatureFlag("flag2", "variant-b"); + + FeatureFlagStore other = new FeatureFlagStore(); + other.addFeatureFlag("flag2", "variant-updated"); + other.addFeatureFlag("flag3", "variant-c"); + + store.merge(other); + + List flags = store.toList(); + assertEquals(3, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("variant-a", flags.get(0).getVariant()); + assertEquals("flag2", flags.get(1).getName()); + assertEquals("variant-updated", flags.get(1).getVariant()); + assertEquals("flag3", flags.get(2).getName()); + assertEquals("variant-c", flags.get(2).getVariant()); + } + + @Test + public void testSize() { + assertEquals(0, store.size()); + + store.addFeatureFlag("flag1", "variant-a"); + assertEquals(1, store.size()); + + store.addFeatureFlag("flag2", "variant-b"); + assertEquals(2, store.size()); + + store.clearFeatureFlag("flag1"); + assertEquals(1, store.size()); + + store.clearFeatureFlags(); + assertEquals(0, store.size()); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java new file mode 100644 index 00000000..08278a5c --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java @@ -0,0 +1,74 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +/** + * Tests for FeatureFlag + */ +public class FeatureFlagTest { + + @Test + public void testFeatureFlagWithVariant() { + FeatureFlag flag = new FeatureFlag("test-flag", "variant-a"); + assertEquals("test-flag", flag.getName()); + assertEquals("variant-a", flag.getVariant()); + } + + @Test + public void testFeatureFlagWithoutVariant() { + FeatureFlag flag = new FeatureFlag("test-flag"); + assertEquals("test-flag", flag.getName()); + assertNull(flag.getVariant()); + } + + @Test + public void testFeatureFlagWithNullVariant() { + FeatureFlag flag = new FeatureFlag("test-flag", null); + assertEquals("test-flag", flag.getName()); + assertNull(flag.getVariant()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFeatureFlagWithNullName() { + new FeatureFlag(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testFeatureFlagWithEmptyName() { + new FeatureFlag(""); + } + + @Test + public void testFeatureFlagEquals() { + FeatureFlag flag1 = new FeatureFlag("test", "variant-a"); + FeatureFlag flag2 = new FeatureFlag("test", "variant-a"); + FeatureFlag flag3 = new FeatureFlag("test", "variant-b"); + FeatureFlag flag4 = new FeatureFlag("other", "variant-a"); + + assertEquals(flag1, flag2); + assertTrue(!flag1.equals(flag3)); + assertTrue(!flag1.equals(flag4)); + } + + @Test + public void testFeatureFlagHashCode() { + FeatureFlag flag1 = new FeatureFlag("test", "variant-a"); + FeatureFlag flag2 = new FeatureFlag("test", "variant-a"); + assertEquals(flag1.hashCode(), flag2.hashCode()); + } + + @Test + public void testFeatureFlagToString() { + FeatureFlag flag = new FeatureFlag("test-flag", "variant-a"); + String result = flag.toString(); + assertTrue(result.contains("test-flag")); + assertTrue(result.contains("variant-a")); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java new file mode 100644 index 00000000..d9932f4f --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java @@ -0,0 +1,208 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for feature flags in Report (Event) + */ +public class ReportFeatureFlagTest { + + private Bugsnag bugsnag; + + @Before + public void setUp() { + bugsnag = new Bugsnag("api-key", false); + } + + @After + public void tearDown() { + bugsnag.close(); + } + + @Test + public void testAddFeatureFlagOnReport() { + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("report-flag", "report-variant"); + + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("report-flag", flags.get(0).getName()); + assertEquals("report-variant", flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlagWithoutVariant() { + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("report-flag"); + + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("report-flag", flags.get(0).getName()); + assertEquals(null, flags.get(0).getVariant()); + } + + @Test + public void testAddFeatureFlags() { + List flagsToAdd = new ArrayList(); + flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); + flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlags(flagsToAdd); + + List flags = report.getFeatureFlags(); + + assertEquals(2, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + } + + @Test + public void testClearFeatureFlag() { + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("flag1", "variant-a"); + report.addFeatureFlag("flag2", "variant-b"); + report.clearFeatureFlag("flag1"); + + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + } + + @Test + public void testClearFeatureFlags() { + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("flag1", "variant-a"); + report.addFeatureFlag("flag2", "variant-b"); + report.clearFeatureFlags(); + + List flags = report.getFeatureFlags(); + + assertEquals(0, flags.size()); + } + + @Test + public void testReportFlagsInheritFromClient() { + bugsnag.addFeatureFlag("client-flag", "client-variant"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("client-flag", flags.get(0).getName()); + assertEquals("client-variant", flags.get(0).getVariant()); + } + + @Test + public void testReportFlagsOverrideClientFlags() { + bugsnag.addFeatureFlag("flag1", "client-variant"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("flag1", "report-variant"); + + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("report-variant", flags.get(0).getVariant()); + } + + @Test + public void testFeatureFlagOrderAcrossAllScopes() { + // Add flags to configuration + bugsnag.getConfig().addFeatureFlag("flag1", "config-variant"); + bugsnag.getConfig().addFeatureFlag("flag2", "config-variant"); + + // Add flags to client (one new, one override) + bugsnag.addFeatureFlag("flag2", "client-variant"); + bugsnag.addFeatureFlag("flag3", "client-variant"); + + // Add flags to report (one new, one override) + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("flag3", "report-variant"); + report.addFeatureFlag("flag4", "report-variant"); + + List flags = report.getFeatureFlags(); + + // Should have all 4 flags in the order they were first added + assertEquals(4, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("config-variant", flags.get(0).getVariant()); + assertEquals("flag2", flags.get(1).getName()); + assertEquals("client-variant", flags.get(1).getVariant()); + assertEquals("flag3", flags.get(2).getName()); + assertEquals("report-variant", flags.get(2).getVariant()); + assertEquals("flag4", flags.get(3).getName()); + assertEquals("report-variant", flags.get(3).getVariant()); + } + + @Test + public void testClearAndReAddChangesPosition() { + bugsnag.getConfig().addFeatureFlag("flag1", "value1"); + bugsnag.getConfig().addFeatureFlag("flag2", "value2"); + bugsnag.getConfig().clearFeatureFlag("flag1"); + + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("flag1", "value1-readded"); + + List flags = report.getFeatureFlags(); + + // flag1 should now be at the end since it was removed and re-added + assertEquals(2, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + assertEquals("flag1", flags.get(1).getName()); + assertEquals("value1-readded", flags.get(1).getVariant()); + } + + @Test + public void testFeatureFlagChaining() { + Report report = bugsnag.buildReport(new RuntimeException("Test")); + + report.addFeatureFlag("flag1", "variant-a") + .addFeatureFlag("flag2", "variant-b") + .addFeatureFlag("flag3"); + + List flags = report.getFeatureFlags(); + + assertEquals(3, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("flag2", flags.get(1).getName()); + assertEquals("flag3", flags.get(2).getName()); + } + + @Test + public void testMultipleScopesMaintainInsertionOrder() { + // Config adds flag1 and flag2 + bugsnag.getConfig().addFeatureFlag("flag1", "value1"); + bugsnag.getConfig().addFeatureFlag("flag2", "value2"); + + // Note: clearing flag from client doesn't remove it from config + // It only affects the client's own feature flag store + // When building a report, config flags are copied first + + // Report adds flag1 with updated value (overrides config value but keeps position) + // and adds flag2 with updated value + Report report = bugsnag.buildReport(new RuntimeException("Test")); + report.addFeatureFlag("flag1", "value1-updated"); + report.addFeatureFlag("flag2", "value2-updated"); + + List flags = report.getFeatureFlags(); + + // Both flags should maintain their original order from config + assertEquals(2, flags.size()); + assertEquals("flag1", flags.get(0).getName()); + assertEquals("value1-updated", flags.get(0).getVariant()); + assertEquals("flag2", flags.get(1).getName()); + assertEquals("value2-updated", flags.get(1).getVariant()); + } +} diff --git a/features/feature_flags.feature b/features/feature_flags.feature new file mode 100644 index 00000000..65b0360a --- /dev/null +++ b/features/feature_flags.feature @@ -0,0 +1,72 @@ +Feature: Feature Flags + +Scenario: Test single feature flag on Java app + When I run "FeatureFlagScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 1 elements + And the error payload field "events.0.featureFlags.0.name" equals "demo_flag" + And the error payload field "events.0.featureFlags.0.variant" equals "variant_a" + +Scenario: Test feature flag set via callback on Java app + When I run "FeatureFlagCallbackScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 1 elements + And the error payload field "events.0.featureFlags.0.name" equals "callback_flag" + And the error payload field "events.0.featureFlags.0.variant" equals "callback_variant" + +Scenario: Test feature flag override on Java app + When I run "FeatureFlagOverrideScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 1 elements + And the error payload field "events.0.featureFlags.0.name" equals "override_flag" + And the error payload field "events.0.featureFlags.0.variant" equals "event_variant" + +Scenario: Test multiple feature flags on Java app + When I run "MultipleFeatureFlagsScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 3 elements + And the error payload field "events.0.featureFlags.0.name" equals "flag_a" + And the error payload field "events.0.featureFlags.0.variant" equals "variant_1" + And the error payload field "events.0.featureFlags.1.name" equals "flag_b" + And the error payload field "events.0.featureFlags.1.variant" is null + And the error payload field "events.0.featureFlags.2.name" equals "flag_c" + And the error payload field "events.0.featureFlags.2.variant" equals "variant_3" + +Scenario: Test clear feature flag on Java app + When I run "ClearFeatureFlagScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 1 elements + And the error payload field "events.0.featureFlags.0.name" equals "flag_to_keep" + And the error payload field "events.0.featureFlags.0.variant" equals "variant" + +Scenario: Test single feature flag on Spring Boot app + When I run spring boot "FeatureFlagScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Spring" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 1 elements + And the error payload field "events.0.featureFlags.0.name" equals "demo_flag" + And the error payload field "events.0.featureFlags.0.variant" equals "variant_a" + +Scenario: Test multiple feature flags on Spring Boot app + When I run spring boot "MultipleFeatureFlagsScenario" with the defaults + And I wait to receive an error + And the error is valid for the error reporting API version "4" for the "Bugsnag Spring" notifier + And the error payload field "events" is an array with 1 elements + And the error payload field "events.0.featureFlags" is an array with 3 elements + And the error payload field "events.0.featureFlags.0.name" equals "flag_a" + And the error payload field "events.0.featureFlags.0.variant" equals "variant_1" + And the error payload field "events.0.featureFlags.1.name" equals "flag_b" + And the error payload field "events.0.featureFlags.1.variant" is null + And the error payload field "events.0.featureFlags.2.name" equals "flag_c" + And the error payload field "events.0.featureFlags.2.variant" equals "variant_3" diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ClearFeatureFlagScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ClearFeatureFlagScenario.java new file mode 100644 index 00000000..891af5a9 --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ClearFeatureFlagScenario.java @@ -0,0 +1,21 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +/** + * Sends a handled exception to Bugsnag demonstrating clear feature flag. + */ +public class ClearFeatureFlagScenario extends Scenario { + + public ClearFeatureFlagScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + bugsnag.addFeatureFlag("flag_to_clear", "variant"); + bugsnag.addFeatureFlag("flag_to_keep", "variant"); + bugsnag.clearFeatureFlag("flag_to_clear"); + bugsnag.notify(generateException()); + } +} diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagCallbackScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagCallbackScenario.java new file mode 100644 index 00000000..2cb7fe06 --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagCallbackScenario.java @@ -0,0 +1,21 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +/** + * Sends a handled exception to Bugsnag with a feature flag set via callback. + */ +public class FeatureFlagCallbackScenario extends Scenario { + + public FeatureFlagCallbackScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + bugsnag.notify(generateException(), report -> { + report.addFeatureFlag("callback_flag", "callback_variant"); + return true; + }); + } +} diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagOverrideScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagOverrideScenario.java new file mode 100644 index 00000000..fcc94279 --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagOverrideScenario.java @@ -0,0 +1,25 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +/** + * Sends a handled exception to Bugsnag demonstrating feature flag override behavior. + */ +public class FeatureFlagOverrideScenario extends Scenario { + + public FeatureFlagOverrideScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + // Add flag at client level + bugsnag.addFeatureFlag("override_flag", "client_variant"); + + // Override the flag at event level + bugsnag.notify(generateException(), report -> { + report.addFeatureFlag("override_flag", "event_variant"); + return true; + }); + } +} diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagScenario.java new file mode 100644 index 00000000..a1aaa3df --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/FeatureFlagScenario.java @@ -0,0 +1,19 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +/** + * Sends a handled exception to Bugsnag with a feature flag. + */ +public class FeatureFlagScenario extends Scenario { + + public FeatureFlagScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + bugsnag.addFeatureFlag("demo_flag", "variant_a"); + bugsnag.notify(generateException()); + } +} diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleFeatureFlagsScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleFeatureFlagsScenario.java new file mode 100644 index 00000000..6a00541f --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleFeatureFlagsScenario.java @@ -0,0 +1,21 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +/** + * Sends a handled exception to Bugsnag demonstrating multiple feature flags. + */ +public class MultipleFeatureFlagsScenario extends Scenario { + + public MultipleFeatureFlagsScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + bugsnag.addFeatureFlag("flag_a", "variant_1"); + bugsnag.addFeatureFlag("flag_b"); + bugsnag.addFeatureFlag("flag_c", "variant_3"); + bugsnag.notify(generateException()); + } +} From d9f4a5d65395910ebe81698ae2dfb1df473a05fa Mon Sep 17 00:00:00 2001 From: richard elms Date: Mon, 15 Dec 2025 12:45:18 +0100 Subject: [PATCH 2/6] remove trailing spaces --- bugsnag/src/main/java/com/bugsnag/Report.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugsnag/src/main/java/com/bugsnag/Report.java b/bugsnag/src/main/java/com/bugsnag/Report.java index bf176551..e66ac6ba 100644 --- a/bugsnag/src/main/java/com/bugsnag/Report.java +++ b/bugsnag/src/main/java/com/bugsnag/Report.java @@ -51,7 +51,7 @@ protected Report(Configuration config, Throwable throwable) { this.handledState = handledState; this.severity = handledState.getOriginalSeverity(); diagnostics = new Diagnostics(this.config); - + // Initialize feature flags: start with config, then merge client flags featureFlagStore = config.copyFeatureFlagStore(); if (clientFeatureFlagStore != null) { From 04ca7f58440de9875502b35078ed9972dd5c2b6a Mon Sep 17 00:00:00 2001 From: richard elms Date: Mon, 15 Dec 2025 12:51:10 +0100 Subject: [PATCH 3/6] checkstyle --- .../com/bugsnag/BugsnagFeatureFlagTest.java | 46 +++++++------- .../bugsnag/ConfigurationFeatureFlagTest.java | 43 +++++++------ .../com/bugsnag/FeatureFlagStoreTest.java | 45 +++++++------- .../java/com/bugsnag/FeatureFlagTest.java | 3 - .../com/bugsnag/ReportFeatureFlagTest.java | 60 +++++++++---------- 5 files changed, 96 insertions(+), 101 deletions(-) diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java index 4563dc81..eb9df064 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java @@ -29,10 +29,10 @@ public void tearDown() { @Test public void testAddFeatureFlag() { bugsnag.addFeatureFlag("flag1", "variant-a"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("variant-a", flags.get(0).getVariant()); @@ -41,10 +41,10 @@ public void testAddFeatureFlag() { @Test public void testAddFeatureFlagWithoutVariant() { bugsnag.addFeatureFlag("flag1"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals(null, flags.get(0).getVariant()); @@ -55,12 +55,12 @@ public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); - + bugsnag.addFeatureFlags(flagsToAdd); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("flag2", flags.get(1).getName()); @@ -71,10 +71,10 @@ public void testClearFeatureFlag() { bugsnag.addFeatureFlag("flag1", "variant-a"); bugsnag.addFeatureFlag("flag2", "variant-b"); bugsnag.clearFeatureFlag("flag1"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag2", flags.get(0).getName()); } @@ -84,10 +84,10 @@ public void testClearFeatureFlags() { bugsnag.addFeatureFlag("flag1", "variant-a"); bugsnag.addFeatureFlag("flag2", "variant-b"); bugsnag.clearFeatureFlags(); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(0, flags.size()); } @@ -95,17 +95,17 @@ public void testClearFeatureFlags() { public void testClientFlagsInheritFromConfiguration() { Configuration config = bugsnag.getConfig(); config.addFeatureFlag("config-flag", "config-variant"); - + Bugsnag client = new Bugsnag("api-key", false); client.getConfig().addFeatureFlag("config-flag", "config-variant"); - + Report report = client.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("config-flag", flags.get(0).getName()); assertEquals("config-variant", flags.get(0).getVariant()); - + client.close(); } @@ -113,18 +113,18 @@ public void testClientFlagsInheritFromConfiguration() { public void testClientFlagsOverrideConfigurationFlags() { Configuration config = bugsnag.getConfig(); config.addFeatureFlag("flag1", "config-variant"); - + Bugsnag client = new Bugsnag("api-key", false); client.getConfig().addFeatureFlag("flag1", "config-variant"); client.addFeatureFlag("flag1", "client-variant"); - + Report report = client.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("client-variant", flags.get(0).getVariant()); - + client.close(); } @@ -133,20 +133,20 @@ public void testFeatureFlagOrderPreservedAcrossScopes() { Configuration config = bugsnag.getConfig(); config.addFeatureFlag("flag1", "config-variant"); config.addFeatureFlag("flag2", "config-variant"); - + Bugsnag client = new Bugsnag("api-key", false); client.getConfig().addFeatureFlag("flag1", "config-variant"); client.getConfig().addFeatureFlag("flag2", "config-variant"); client.addFeatureFlag("flag3", "client-variant"); - + Report report = client.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(3, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("flag2", flags.get(1).getName()); assertEquals("flag3", flags.get(2).getName()); - + client.close(); } } diff --git a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java index d079315f..6805fcbd 100644 --- a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java @@ -1,7 +1,6 @@ package com.bugsnag; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; @@ -24,35 +23,35 @@ public void setUp() { @Test public void testAddFeatureFlag() { config.addFeatureFlag("flag1", "variant-a"); - + // Verify by creating a client and checking the flags are inherited Bugsnag bugsnag = new Bugsnag("api-key"); bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("variant-a", flags.get(0).getVariant()); - + bugsnag.close(); } @Test public void testAddFeatureFlagWithoutVariant() { config.addFeatureFlag("flag1"); - + Bugsnag bugsnag = new Bugsnag("api-key"); bugsnag.getConfig().addFeatureFlag("flag1"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals(null, flags.get(0).getVariant()); - + bugsnag.close(); } @@ -61,19 +60,19 @@ public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); - + config.addFeatureFlags(flagsToAdd); - + Bugsnag bugsnag = new Bugsnag("api-key"); bugsnag.getConfig().addFeatureFlags(flagsToAdd); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("flag2", flags.get(1).getName()); - + bugsnag.close(); } @@ -82,18 +81,18 @@ public void testClearFeatureFlag() { config.addFeatureFlag("flag1", "variant-a"); config.addFeatureFlag("flag2", "variant-b"); config.clearFeatureFlag("flag1"); - + Bugsnag bugsnag = new Bugsnag("api-key"); bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); bugsnag.getConfig().addFeatureFlag("flag2", "variant-b"); bugsnag.getConfig().clearFeatureFlag("flag1"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag2", flags.get(0).getName()); - + bugsnag.close(); } @@ -102,17 +101,17 @@ public void testClearFeatureFlags() { config.addFeatureFlag("flag1", "variant-a"); config.addFeatureFlag("flag2", "variant-b"); config.clearFeatureFlags(); - + Bugsnag bugsnag = new Bugsnag("api-key"); bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); bugsnag.getConfig().addFeatureFlag("flag2", "variant-b"); bugsnag.getConfig().clearFeatureFlags(); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(0, flags.size()); - + bugsnag.close(); } } diff --git a/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java index 75905f70..04746bb9 100644 --- a/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java @@ -1,7 +1,6 @@ package com.bugsnag; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import org.junit.Before; import org.junit.Test; @@ -43,7 +42,7 @@ public void testAddFeatureFlagWithNullVariant() { public void testAddFeatureFlagUpdatesVariant() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag1", "variant-b"); - + List flags = store.toList(); assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); @@ -55,7 +54,7 @@ public void testAddFeatureFlagMaintainsOrder() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); store.addFeatureFlag("flag3", "variant-c"); - + List flags = store.toList(); assertEquals(3, flags.size()); assertEquals("flag1", flags.get(0).getName()); @@ -68,7 +67,7 @@ public void testAddFeatureFlagUpdateDoesNotChangeOrder() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); store.addFeatureFlag("flag1", "variant-updated"); - + List flags = store.toList(); assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); @@ -81,9 +80,9 @@ public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); - + store.addFeatureFlags(flagsToAdd); - + List flags = store.toList(); assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); @@ -94,9 +93,9 @@ public void testAddFeatureFlags() { public void testClearFeatureFlag() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); - + store.clearFeatureFlag("flag1"); - + List flags = store.toList(); assertEquals(1, flags.size()); assertEquals("flag2", flags.get(0).getName()); @@ -106,10 +105,10 @@ public void testClearFeatureFlag() { public void testClearFeatureFlagAndReAdd() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); - + store.clearFeatureFlag("flag1"); store.addFeatureFlag("flag1", "variant-updated"); - + List flags = store.toList(); assertEquals(2, flags.size()); assertEquals("flag2", flags.get(0).getName()); @@ -120,9 +119,9 @@ public void testClearFeatureFlagAndReAdd() { public void testClearFeatureFlags() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); - + store.clearFeatureFlags(); - + List flags = store.toList(); assertEquals(0, flags.size()); } @@ -131,16 +130,16 @@ public void testClearFeatureFlags() { public void testCopy() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); - + FeatureFlagStore copy = store.copy(); - + List originalFlags = store.toList(); List copiedFlags = copy.toList(); - + assertEquals(originalFlags.size(), copiedFlags.size()); assertEquals("flag1", copiedFlags.get(0).getName()); assertEquals("flag2", copiedFlags.get(1).getName()); - + // Verify that modifying the copy doesn't affect the original copy.addFeatureFlag("flag3", "variant-c"); assertEquals(2, store.toList().size()); @@ -151,13 +150,13 @@ public void testCopy() { public void testMerge() { store.addFeatureFlag("flag1", "variant-a"); store.addFeatureFlag("flag2", "variant-b"); - + FeatureFlagStore other = new FeatureFlagStore(); other.addFeatureFlag("flag2", "variant-updated"); other.addFeatureFlag("flag3", "variant-c"); - + store.merge(other); - + List flags = store.toList(); assertEquals(3, flags.size()); assertEquals("flag1", flags.get(0).getName()); @@ -171,16 +170,16 @@ public void testMerge() { @Test public void testSize() { assertEquals(0, store.size()); - + store.addFeatureFlag("flag1", "variant-a"); assertEquals(1, store.size()); - + store.addFeatureFlag("flag2", "variant-b"); assertEquals(2, store.size()); - + store.clearFeatureFlag("flag1"); assertEquals(1, store.size()); - + store.clearFeatureFlags(); assertEquals(0, store.size()); } diff --git a/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java index 08278a5c..fca385fc 100644 --- a/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java @@ -1,14 +1,11 @@ package com.bugsnag; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import org.junit.Test; -import java.util.List; - /** * Tests for FeatureFlag */ diff --git a/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java index d9932f4f..4c1e3c39 100644 --- a/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java @@ -30,9 +30,9 @@ public void tearDown() { public void testAddFeatureFlagOnReport() { Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlag("report-flag", "report-variant"); - + List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("report-flag", flags.get(0).getName()); assertEquals("report-variant", flags.get(0).getVariant()); @@ -42,9 +42,9 @@ public void testAddFeatureFlagOnReport() { public void testAddFeatureFlagWithoutVariant() { Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlag("report-flag"); - + List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("report-flag", flags.get(0).getName()); assertEquals(null, flags.get(0).getVariant()); @@ -55,12 +55,12 @@ public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlags(flagsToAdd); - + List flags = report.getFeatureFlags(); - + assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("flag2", flags.get(1).getName()); @@ -72,9 +72,9 @@ public void testClearFeatureFlag() { report.addFeatureFlag("flag1", "variant-a"); report.addFeatureFlag("flag2", "variant-b"); report.clearFeatureFlag("flag1"); - + List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag2", flags.get(0).getName()); } @@ -85,19 +85,19 @@ public void testClearFeatureFlags() { report.addFeatureFlag("flag1", "variant-a"); report.addFeatureFlag("flag2", "variant-b"); report.clearFeatureFlags(); - + List flags = report.getFeatureFlags(); - + assertEquals(0, flags.size()); } @Test public void testReportFlagsInheritFromClient() { bugsnag.addFeatureFlag("client-flag", "client-variant"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("client-flag", flags.get(0).getName()); assertEquals("client-variant", flags.get(0).getVariant()); @@ -106,12 +106,12 @@ public void testReportFlagsInheritFromClient() { @Test public void testReportFlagsOverrideClientFlags() { bugsnag.addFeatureFlag("flag1", "client-variant"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlag("flag1", "report-variant"); - + List flags = report.getFeatureFlags(); - + assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("report-variant", flags.get(0).getVariant()); @@ -122,18 +122,18 @@ public void testFeatureFlagOrderAcrossAllScopes() { // Add flags to configuration bugsnag.getConfig().addFeatureFlag("flag1", "config-variant"); bugsnag.getConfig().addFeatureFlag("flag2", "config-variant"); - + // Add flags to client (one new, one override) bugsnag.addFeatureFlag("flag2", "client-variant"); bugsnag.addFeatureFlag("flag3", "client-variant"); - + // Add flags to report (one new, one override) Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlag("flag3", "report-variant"); report.addFeatureFlag("flag4", "report-variant"); - + List flags = report.getFeatureFlags(); - + // Should have all 4 flags in the order they were first added assertEquals(4, flags.size()); assertEquals("flag1", flags.get(0).getName()); @@ -151,12 +151,12 @@ public void testClearAndReAddChangesPosition() { bugsnag.getConfig().addFeatureFlag("flag1", "value1"); bugsnag.getConfig().addFeatureFlag("flag2", "value2"); bugsnag.getConfig().clearFeatureFlag("flag1"); - + Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlag("flag1", "value1-readded"); - + List flags = report.getFeatureFlags(); - + // flag1 should now be at the end since it was removed and re-added assertEquals(2, flags.size()); assertEquals("flag2", flags.get(0).getName()); @@ -167,13 +167,13 @@ public void testClearAndReAddChangesPosition() { @Test public void testFeatureFlagChaining() { Report report = bugsnag.buildReport(new RuntimeException("Test")); - + report.addFeatureFlag("flag1", "variant-a") .addFeatureFlag("flag2", "variant-b") .addFeatureFlag("flag3"); - + List flags = report.getFeatureFlags(); - + assertEquals(3, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("flag2", flags.get(1).getName()); @@ -185,19 +185,19 @@ public void testMultipleScopesMaintainInsertionOrder() { // Config adds flag1 and flag2 bugsnag.getConfig().addFeatureFlag("flag1", "value1"); bugsnag.getConfig().addFeatureFlag("flag2", "value2"); - + // Note: clearing flag from client doesn't remove it from config // It only affects the client's own feature flag store // When building a report, config flags are copied first - + // Report adds flag1 with updated value (overrides config value but keeps position) // and adds flag2 with updated value Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlag("flag1", "value1-updated"); report.addFeatureFlag("flag2", "value2-updated"); - + List flags = report.getFeatureFlags(); - + // Both flags should maintain their original order from config assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); From a2ebaf9d7f210884af4ae0d1068aa61dfa03d782 Mon Sep 17 00:00:00 2001 From: richard elms Date: Mon, 15 Dec 2025 13:16:26 +0100 Subject: [PATCH 4/6] appender --- .../java/com/bugsnag/BugsnagAppender.java | 76 ++++++++++++++- .../bugsnag/logback/LogbackFeatureFlag.java | 38 ++++++++ .../test/java/com/bugsnag/AppenderTest.java | 97 +++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 bugsnag/src/main/java/com/bugsnag/logback/LogbackFeatureFlag.java diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 07f42b56..73566d63 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -3,6 +3,7 @@ import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.logback.BugsnagMarker; +import com.bugsnag.logback.LogbackFeatureFlag; import com.bugsnag.logback.LogbackMetadata; import com.bugsnag.logback.LogbackMetadataKey; import com.bugsnag.logback.LogbackMetadataTab; @@ -74,9 +75,11 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase { /** Application version. */ private String appVersion; - private List globalMetadata = new ArrayList(); + /** Feature flags configured via logback.xml. */ + private List featureFlags = new ArrayList(); + /** Bugsnag client. */ private Bugsnag bugsnag = null; @@ -271,6 +274,11 @@ private Bugsnag createBugsnag() { bugsnag.setProjectPackages(projectPackages.toArray(new String[0])); bugsnag.setSendThreads(sendThreads); + // Add feature flags + for (LogbackFeatureFlag flag : featureFlags) { + bugsnag.addFeatureFlag(flag.getName(), flag.getVariant()); + } + // Add a callback to put global metadata on every report bugsnag.addCallback(new Callback() { @Override @@ -592,4 +600,70 @@ private boolean isExcludedLogger(String loggerName) { } return false; } + + /** + * Add a feature flag with a name and variant. + * This is typically configured via logback.xml. + * + * @param name the feature flag name + * @param variant the feature flag variant (can be null) + */ + public void addFeatureFlag(String name, String variant) { + LogbackFeatureFlag flag = new LogbackFeatureFlag(); + flag.setName(name); + flag.setVariant(variant); + featureFlags.add(flag); + + if (bugsnag != null) { + bugsnag.addFeatureFlag(name, variant); + } + } + + /** + * Add a feature flag with just a name (no variant). + * This is typically configured via logback.xml. + * + * @param name the feature flag name + */ + public void addFeatureFlag(String name) { + addFeatureFlag(name, null); + } + + /** + * Add a feature flag from logback.xml configuration. + * Internal use only - should only be used via the logback.xml file. + * + * @param flag the feature flag to add + */ + public void setFeatureFlag(LogbackFeatureFlag flag) { + featureFlags.add(flag); + + if (bugsnag != null) { + bugsnag.addFeatureFlag(flag.getName(), flag.getVariant()); + } + } + + /** + * Clear a feature flag by name. + * + * @param name the feature flag name to remove + */ + public void clearFeatureFlag(String name) { + featureFlags.removeIf(flag -> flag.getName().equals(name)); + + if (bugsnag != null) { + bugsnag.clearFeatureFlag(name); + } + } + + /** + * Clear all feature flags. + */ + public void clearFeatureFlags() { + featureFlags.clear(); + + if (bugsnag != null) { + bugsnag.clearFeatureFlags(); + } + } } diff --git a/bugsnag/src/main/java/com/bugsnag/logback/LogbackFeatureFlag.java b/bugsnag/src/main/java/com/bugsnag/logback/LogbackFeatureFlag.java new file mode 100644 index 00000000..9315464c --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/logback/LogbackFeatureFlag.java @@ -0,0 +1,38 @@ +package com.bugsnag.logback; + +/** + * Used to allow feature flags to be configured in the logback.xml file. + */ +public class LogbackFeatureFlag { + + private String name; + private String variant; + + /** + * @return the name of the feature flag + */ + public String getName() { + return name; + } + + /** + * @param name the name of the feature flag + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the variant of the feature flag + */ + public String getVariant() { + return variant; + } + + /** + * @param variant the variant of the feature flag + */ + public void setVariant(String variant) { + this.variant = variant; + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index e0e5756a..7e079197 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -56,6 +56,9 @@ public void swapDelivery() { originalSessionDelivery = bugsnag.getSessionDelivery(); sessionDelivery = new StubSessionDelivery(); bugsnag.setSessionDelivery(sessionDelivery); + + // Clear any feature flags from previous tests + appender.clearFeatureFlags(); } /** @@ -414,4 +417,98 @@ private SessionTracker getSessionTracker(Bugsnag bugsnag) { private Map getMetadataMap(Notification notification, String key) { return ((Map) notification.getEvents().get(0).getMetadata().get(key)); } + + @Test + public void testFeatureFlagConfiguration() { + // Add feature flags programmatically (XML configuration will be tested separately) + appender.addFeatureFlag("sample_group", "a"); + appender.addFeatureFlag("another_feature"); + + // Send a log message + LOGGER.warn("Exception with feature flags", new RuntimeException("test")); + + // Check that a report was sent to Bugsnag + assertEquals(1, delivery.getNotifications().size()); + + Notification notification = delivery.getNotifications().get(0); + List featureFlags = notification.getEvents().get(0).getFeatureFlags(); + + // Check that feature flags are present + assertEquals(2, featureFlags.size()); + + // Check first feature flag + assertEquals("sample_group", featureFlags.get(0).getName()); + assertEquals("a", featureFlags.get(0).getVariant()); + + // Check second feature flag + assertEquals("another_feature", featureFlags.get(1).getName()); + assertEquals(null, featureFlags.get(1).getVariant()); + } + + @Test + public void testAddFeatureFlagProgrammatically() { + // Add a feature flag programmatically + appender.addFeatureFlag("runtime_feature", "variant_b"); + + // Send a log message + LOGGER.warn("Exception with runtime feature flag", new RuntimeException("test")); + + // Check that a report was sent to Bugsnag + assertEquals(1, delivery.getNotifications().size()); + + Notification notification = delivery.getNotifications().get(0); + List featureFlags = notification.getEvents().get(0).getFeatureFlags(); + + // Should have 1 programmatic feature flag + assertEquals(1, featureFlags.size()); + + // Check the programmatically added flag is present + assertEquals("runtime_feature", featureFlags.get(0).getName()); + assertEquals("variant_b", featureFlags.get(0).getVariant()); + } + + @Test + public void testClearFeatureFlag() { + // Add some feature flags first + appender.addFeatureFlag("sample_group", "a"); + appender.addFeatureFlag("another_feature"); + + // Clear a specific feature flag + appender.clearFeatureFlag("sample_group"); + + // Send a log message + LOGGER.warn("Exception after clearing feature flag", new RuntimeException("test")); + + // Check that a report was sent to Bugsnag + assertEquals(1, delivery.getNotifications().size()); + + Notification notification = delivery.getNotifications().get(0); + List featureFlags = notification.getEvents().get(0).getFeatureFlags(); + + // Should only have 1 feature flag (another_feature) remaining + assertEquals(1, featureFlags.size()); + assertEquals("another_feature", featureFlags.get(0).getName()); + } + + @Test + public void testClearAllFeatureFlags() { + // Add some feature flags first + appender.addFeatureFlag("sample_group", "a"); + appender.addFeatureFlag("another_feature"); + + // Clear all feature flags + appender.clearFeatureFlags(); + + // Send a log message + LOGGER.warn("Exception after clearing all feature flags", new RuntimeException("test")); + + // Check that a report was sent to Bugsnag + assertEquals(1, delivery.getNotifications().size()); + + Notification notification = delivery.getNotifications().get(0); + List featureFlags = notification.getEvents().get(0).getFeatureFlags(); + + // Should have no feature flags + assertEquals(0, featureFlags.size()); + } } From 9c38ec3990b52e63a3f47e41c3b9de15947adb65 Mon Sep 17 00:00:00 2001 From: richard elms Date: Tue, 16 Dec 2025 09:45:16 +0100 Subject: [PATCH 5/6] test fixes --- .../java/com/bugsnag/BugsnagAppender.java | 2 +- .../bugsnag/ConfigurationFeatureFlagTest.java | 41 +++---------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 73566d63..01aec525 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -649,7 +649,7 @@ public void setFeatureFlag(LogbackFeatureFlag flag) { * @param name the feature flag name to remove */ public void clearFeatureFlag(String name) { - featureFlags.removeIf(flag -> flag.getName().equals(name)); + featureFlags.removeIf(flag -> flag.getName() != null && flag.getName().equals(name)); if (bugsnag != null) { bugsnag.clearFeatureFlag(name); diff --git a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java index 6805fcbd..d86179dc 100644 --- a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java @@ -24,35 +24,25 @@ public void setUp() { public void testAddFeatureFlag() { config.addFeatureFlag("flag1", "variant-a"); - // Verify by creating a client and checking the flags are inherited - Bugsnag bugsnag = new Bugsnag("api-key"); - bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); - - Report report = bugsnag.buildReport(new RuntimeException("Test")); + // Verify the config has the flag + Report report = new Report(config, new RuntimeException("Test")); List flags = report.getFeatureFlags(); assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("variant-a", flags.get(0).getVariant()); - - bugsnag.close(); } @Test public void testAddFeatureFlagWithoutVariant() { config.addFeatureFlag("flag1"); - Bugsnag bugsnag = new Bugsnag("api-key"); - bugsnag.getConfig().addFeatureFlag("flag1"); - - Report report = bugsnag.buildReport(new RuntimeException("Test")); + Report report = new Report(config, new RuntimeException("Test")); List flags = report.getFeatureFlags(); assertEquals(1, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals(null, flags.get(0).getVariant()); - - bugsnag.close(); } @Test @@ -63,17 +53,12 @@ public void testAddFeatureFlags() { config.addFeatureFlags(flagsToAdd); - Bugsnag bugsnag = new Bugsnag("api-key"); - bugsnag.getConfig().addFeatureFlags(flagsToAdd); - - Report report = bugsnag.buildReport(new RuntimeException("Test")); + Report report = new Report(config, new RuntimeException("Test")); List flags = report.getFeatureFlags(); assertEquals(2, flags.size()); assertEquals("flag1", flags.get(0).getName()); assertEquals("flag2", flags.get(1).getName()); - - bugsnag.close(); } @Test @@ -82,18 +67,11 @@ public void testClearFeatureFlag() { config.addFeatureFlag("flag2", "variant-b"); config.clearFeatureFlag("flag1"); - Bugsnag bugsnag = new Bugsnag("api-key"); - bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); - bugsnag.getConfig().addFeatureFlag("flag2", "variant-b"); - bugsnag.getConfig().clearFeatureFlag("flag1"); - - Report report = bugsnag.buildReport(new RuntimeException("Test")); + Report report = new Report(config, new RuntimeException("Test")); List flags = report.getFeatureFlags(); assertEquals(1, flags.size()); assertEquals("flag2", flags.get(0).getName()); - - bugsnag.close(); } @Test @@ -102,16 +80,9 @@ public void testClearFeatureFlags() { config.addFeatureFlag("flag2", "variant-b"); config.clearFeatureFlags(); - Bugsnag bugsnag = new Bugsnag("api-key"); - bugsnag.getConfig().addFeatureFlag("flag1", "variant-a"); - bugsnag.getConfig().addFeatureFlag("flag2", "variant-b"); - bugsnag.getConfig().clearFeatureFlags(); - - Report report = bugsnag.buildReport(new RuntimeException("Test")); + Report report = new Report(config, new RuntimeException("Test")); List flags = report.getFeatureFlags(); assertEquals(0, flags.size()); - - bugsnag.close(); } } From d06aa20ce03d2fa137163e1dac4abdc9437d1e73 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 11 Mar 2026 14:00:23 +0000 Subject: [PATCH 6/6] fix(FeatureFlag): added a ReadWriteLock to the FeatureFlagStore, and replaced FeatureFlag constructor with `of` factory method --- .../main/java/com/bugsnag/FeatureFlag.java | 40 +++++-- .../java/com/bugsnag/FeatureFlagStore.java | 109 +++++++++++++----- .../com/bugsnag/BugsnagFeatureFlagTest.java | 4 +- .../bugsnag/ConfigurationFeatureFlagTest.java | 4 +- .../com/bugsnag/FeatureFlagStoreTest.java | 4 +- .../java/com/bugsnag/FeatureFlagTest.java | 28 ++--- .../com/bugsnag/ReportFeatureFlagTest.java | 4 +- 7 files changed, 134 insertions(+), 59 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java b/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java index 0df5b633..8f2355fe 100644 --- a/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java +++ b/bugsnag/src/main/java/com/bugsnag/FeatureFlag.java @@ -13,22 +13,13 @@ public class FeatureFlag { private final String name; private final String variant; - /** - * Create a feature flag with a name and no variant. - * - * @param name the name of the feature flag - */ - public FeatureFlag(String name) { - this(name, null); - } - /** * Create a feature flag with a name and variant. * - * @param name the name of the feature flag + * @param name the name of the feature flag * @param variant the variant of the feature flag (can be null) */ - public FeatureFlag(String name, String variant) { + private FeatureFlag(String name, String variant) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Feature flag name cannot be null or empty"); } @@ -77,4 +68,31 @@ public int hashCode() { public String toString() { return "FeatureFlag{name='" + name + "', variant='" + variant + "'}"; } + + /** + * Create a feature flag with a name and no vairant. + * + * @param name the name of the feature flag + */ + public static FeatureFlag of(String name) { + if (name == null || name.isEmpty()) { + return null; + } + + return new FeatureFlag(name, null); + } + + /** + * Create a feature flag with a name and variant. + * + * @param name the name of the feature flag + * @param variant the variant of the feature flag (can be null) + */ + public static FeatureFlag of(String name, String variant) { + if (name == null || name.isEmpty()) { + return null; + } + + return new FeatureFlag(name, variant); + } } diff --git a/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java b/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java index ffd52c30..a3e83ec5 100644 --- a/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java +++ b/bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java @@ -5,6 +5,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Internal storage for feature flags that maintains insertion order. @@ -13,19 +15,25 @@ class FeatureFlagStore { // LinkedHashMap maintains insertion order private final Map flags = new LinkedHashMap(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); /** * Add a feature flag with the specified name and variant. * If the name already exists, the variant will be updated without changing position. * - * @param name the feature flag name + * @param name the feature flag name * @param variant the feature flag variant (can be null) */ - synchronized void addFeatureFlag(String name, String variant) { + void addFeatureFlag(String name, String variant) { if (name == null || name.isEmpty()) { return; } - flags.put(name, variant); + lock.writeLock().lock(); + try { + flags.put(name, variant); + } finally { + lock.writeLock().unlock(); + } } /** @@ -34,14 +42,20 @@ synchronized void addFeatureFlag(String name, String variant) { * * @param featureFlags the feature flags to add */ - synchronized void addFeatureFlags(Collection featureFlags) { - if (featureFlags == null) { + void addFeatureFlags(Collection featureFlags) { + if (featureFlags == null || featureFlags.isEmpty()) { return; } - for (FeatureFlag flag : featureFlags) { - if (flag != null) { - addFeatureFlag(flag.getName(), flag.getVariant()); + + lock.writeLock().lock(); + try { + for (FeatureFlag flag : featureFlags) { + if (flag != null) { + flags.put(flag.getName(), flag.getVariant()); + } } + } finally { + lock.writeLock().unlock(); } } @@ -50,17 +64,27 @@ synchronized void addFeatureFlags(Collection featureFlags) { * * @param name the feature flag name to remove */ - synchronized void clearFeatureFlag(String name) { + void clearFeatureFlag(String name) { if (name != null) { - flags.remove(name); + lock.writeLock().lock(); + try { + flags.remove(name); + } finally { + lock.writeLock().unlock(); + } } } /** * Remove all feature flags. */ - synchronized void clearFeatureFlags() { - flags.clear(); + void clearFeatureFlags() { + lock.writeLock().lock(); + try { + flags.clear(); + } finally { + lock.writeLock().unlock(); + } } /** @@ -68,12 +92,20 @@ synchronized void clearFeatureFlags() { * * @return an unmodifiable list of feature flags */ - synchronized List toList() { - List result = new ArrayList(flags.size()); - for (Map.Entry entry : flags.entrySet()) { - result.add(new FeatureFlag(entry.getKey(), entry.getValue())); + List toList() { + lock.readLock().lock(); + try { + List result = new ArrayList<>(flags.size()); + for (Map.Entry entry : flags.entrySet()) { + FeatureFlag flag = FeatureFlag.of(entry.getKey(), entry.getValue()); + if (flag != null) { + result.add(flag); + } + } + return result; + } finally { + lock.readLock().unlock(); } - return result; } /** @@ -81,9 +113,14 @@ synchronized List toList() { * * @return a new FeatureFlagStore with the same flags */ - synchronized FeatureFlagStore copy() { + FeatureFlagStore copy() { FeatureFlagStore copy = new FeatureFlagStore(); - copy.flags.putAll(this.flags); + lock.readLock().lock(); + try { + copy.flags.putAll(this.flags); + } finally { + lock.readLock().unlock(); + } return copy; } @@ -94,14 +131,29 @@ synchronized FeatureFlagStore copy() { * * @param other the other store to merge from */ - synchronized void merge(FeatureFlagStore other) { - if (other == null) { + void merge(FeatureFlagStore other) { + if (other == null || other == this) { return; } - synchronized (other) { - for (Map.Entry entry : other.flags.entrySet()) { - flags.put(entry.getKey(), entry.getValue()); + + // Warning: this *looks* like a classic deadlock pattern, but because this method is only ever called + // with isolated copies of FeatureFlagStore, the locks will never actually be contended. + // If this method were to be called with two live stores, then it would be possible for a deadlock to occur. + other.lock.readLock().lock(); + try { + // we don't use other.size() because it grabs a lock.readLock() again + if (other.flags.isEmpty()) { + return; } + + lock.writeLock().lock(); + try { + flags.putAll(other.flags); + } finally { + lock.writeLock().unlock(); + } + } finally { + other.lock.readLock().unlock(); } } @@ -110,7 +162,12 @@ synchronized void merge(FeatureFlagStore other) { * * @return the number of feature flags */ - synchronized int size() { - return flags.size(); + int size() { + lock.readLock().lock(); + try { + return flags.size(); + } finally { + lock.readLock().unlock(); + } } } diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java index eb9df064..ea8025ff 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java @@ -53,8 +53,8 @@ public void testAddFeatureFlagWithoutVariant() { @Test public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); - flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); - flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + flagsToAdd.add(FeatureFlag.of("flag1", "variant-a")); + flagsToAdd.add(FeatureFlag.of("flag2", "variant-b")); bugsnag.addFeatureFlags(flagsToAdd); diff --git a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java index d86179dc..0533d893 100644 --- a/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java @@ -48,8 +48,8 @@ public void testAddFeatureFlagWithoutVariant() { @Test public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); - flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); - flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + flagsToAdd.add(FeatureFlag.of("flag1", "variant-a")); + flagsToAdd.add(FeatureFlag.of("flag2", "variant-b")); config.addFeatureFlags(flagsToAdd); diff --git a/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java index 04746bb9..34b3cd9d 100644 --- a/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java @@ -78,8 +78,8 @@ public void testAddFeatureFlagUpdateDoesNotChangeOrder() { @Test public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); - flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); - flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + flagsToAdd.add(FeatureFlag.of("flag1", "variant-a")); + flagsToAdd.add(FeatureFlag.of("flag2", "variant-b")); store.addFeatureFlags(flagsToAdd); diff --git a/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java index fca385fc..c4af2062 100644 --- a/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java @@ -13,41 +13,41 @@ public class FeatureFlagTest { @Test public void testFeatureFlagWithVariant() { - FeatureFlag flag = new FeatureFlag("test-flag", "variant-a"); + FeatureFlag flag = FeatureFlag.of("test-flag", "variant-a"); assertEquals("test-flag", flag.getName()); assertEquals("variant-a", flag.getVariant()); } @Test public void testFeatureFlagWithoutVariant() { - FeatureFlag flag = new FeatureFlag("test-flag"); + FeatureFlag flag = FeatureFlag.of("test-flag"); assertEquals("test-flag", flag.getName()); assertNull(flag.getVariant()); } @Test public void testFeatureFlagWithNullVariant() { - FeatureFlag flag = new FeatureFlag("test-flag", null); + FeatureFlag flag = FeatureFlag.of("test-flag", null); assertEquals("test-flag", flag.getName()); assertNull(flag.getVariant()); } - @Test(expected = IllegalArgumentException.class) + @Test public void testFeatureFlagWithNullName() { - new FeatureFlag(null); + assertNull(FeatureFlag.of(null)); } - @Test(expected = IllegalArgumentException.class) + @Test public void testFeatureFlagWithEmptyName() { - new FeatureFlag(""); + assertNull(FeatureFlag.of("")); } @Test public void testFeatureFlagEquals() { - FeatureFlag flag1 = new FeatureFlag("test", "variant-a"); - FeatureFlag flag2 = new FeatureFlag("test", "variant-a"); - FeatureFlag flag3 = new FeatureFlag("test", "variant-b"); - FeatureFlag flag4 = new FeatureFlag("other", "variant-a"); + FeatureFlag flag1 = FeatureFlag.of("test", "variant-a"); + FeatureFlag flag2 = FeatureFlag.of("test", "variant-a"); + FeatureFlag flag3 = FeatureFlag.of("test", "variant-b"); + FeatureFlag flag4 = FeatureFlag.of("other", "variant-a"); assertEquals(flag1, flag2); assertTrue(!flag1.equals(flag3)); @@ -56,14 +56,14 @@ public void testFeatureFlagEquals() { @Test public void testFeatureFlagHashCode() { - FeatureFlag flag1 = new FeatureFlag("test", "variant-a"); - FeatureFlag flag2 = new FeatureFlag("test", "variant-a"); + FeatureFlag flag1 = FeatureFlag.of("test", "variant-a"); + FeatureFlag flag2 = FeatureFlag.of("test", "variant-a"); assertEquals(flag1.hashCode(), flag2.hashCode()); } @Test public void testFeatureFlagToString() { - FeatureFlag flag = new FeatureFlag("test-flag", "variant-a"); + FeatureFlag flag = FeatureFlag.of("test-flag", "variant-a"); String result = flag.toString(); assertTrue(result.contains("test-flag")); assertTrue(result.contains("variant-a")); diff --git a/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java b/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java index 4c1e3c39..94dc9109 100644 --- a/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java @@ -53,8 +53,8 @@ public void testAddFeatureFlagWithoutVariant() { @Test public void testAddFeatureFlags() { List flagsToAdd = new ArrayList(); - flagsToAdd.add(new FeatureFlag("flag1", "variant-a")); - flagsToAdd.add(new FeatureFlag("flag2", "variant-b")); + flagsToAdd.add(FeatureFlag.of("flag1", "variant-a")); + flagsToAdd.add(FeatureFlag.of("flag2", "variant-b")); Report report = bugsnag.buildReport(new RuntimeException("Test")); report.addFeatureFlags(flagsToAdd);