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/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 07f42b56..01aec525 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() != null && 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/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..e66ac6ba 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,17 +36,28 @@ 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; Map allStackTraces = Thread.getAllStackTraces(); @@ -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/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()); + } } 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..eb9df064 --- /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..d86179dc --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java @@ -0,0 +1,88 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; + +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 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()); + } + + @Test + public void testAddFeatureFlagWithoutVariant() { + config.addFeatureFlag("flag1"); + + 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()); + } + + @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); + + 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()); + } + + @Test + public void testClearFeatureFlag() { + config.addFeatureFlag("flag1", "variant-a"); + config.addFeatureFlag("flag2", "variant-b"); + config.clearFeatureFlag("flag1"); + + Report report = new Report(config, new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(1, flags.size()); + assertEquals("flag2", flags.get(0).getName()); + } + + @Test + public void testClearFeatureFlags() { + config.addFeatureFlag("flag1", "variant-a"); + config.addFeatureFlag("flag2", "variant-b"); + config.clearFeatureFlags(); + + Report report = new Report(config, new RuntimeException("Test")); + List flags = report.getFeatureFlags(); + + assertEquals(0, flags.size()); + } +} 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..04746bb9 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java @@ -0,0 +1,186 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; + +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..fca385fc --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java @@ -0,0 +1,71 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * 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..4c1e3c39 --- /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()); + } +}