Skip to content

Commit a884744

Browse files
authored
Merge pull request #247 from bugsnag/featureFlags
Feature flags
2 parents e343ab9 + d06aa20 commit a884744

19 files changed

Lines changed: 1565 additions & 5 deletions

bugsnag/src/main/java/com/bugsnag/Bugsnag.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.io.Closeable;
1212
import java.lang.Thread.UncaughtExceptionHandler;
1313
import java.net.Proxy;
14+
import java.util.Collection;
1415
import java.util.Collections;
1516
import java.util.Date;
1617
import java.util.Set;
@@ -57,6 +58,7 @@ public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
5758

5859
private Configuration config;
5960
private final SessionTracker sessionTracker;
61+
private final FeatureFlagStore featureFlagStore;
6062

6163
private static final ThreadLocal<Metadata> THREAD_METADATA = new ThreadLocal<Metadata>() {
6264
@Override
@@ -91,6 +93,7 @@ public Bugsnag(String apiKey, boolean sendUncaughtExceptions) {
9193

9294
config = new Configuration(apiKey);
9395
sessionTracker = new SessionTracker(config);
96+
featureFlagStore = config.copyFeatureFlagStore();
9497

9598
// Automatically send unhandled exceptions to Bugsnag using this Bugsnag
9699
config.setSendUncaughtExceptions(sendUncaughtExceptions);
@@ -348,7 +351,9 @@ public void setTimeout(int timeout) {
348351
* @see #notify(com.bugsnag.Report)
349352
*/
350353
public Report buildReport(Throwable throwable) {
351-
return new Report(config, throwable);
354+
HandledState handledState = HandledState.newInstance(
355+
HandledState.SeverityReasonType.REASON_HANDLED_EXCEPTION);
356+
return new Report(config, throwable, handledState, Thread.currentThread(), featureFlagStore);
352357
}
353358

354359
/**
@@ -404,7 +409,7 @@ public boolean notify(Throwable throwable, Severity severity, Callback callback)
404409

405410
HandledState handledState = HandledState.newInstance(
406411
HandledState.SeverityReasonType.REASON_USER_SPECIFIED, severity);
407-
Report report = new Report(config, throwable, handledState, Thread.currentThread());
412+
Report report = new Report(config, throwable, handledState, Thread.currentThread(), featureFlagStore);
408413
return notify(report, callback);
409414
}
410415

@@ -422,7 +427,7 @@ public boolean notify(Report report) {
422427
}
423428

424429
boolean notify(Throwable throwable, HandledState handledState, Thread currentThread) {
425-
Report report = new Report(config, throwable, handledState, currentThread);
430+
Report report = new Report(config, throwable, handledState, currentThread, featureFlagStore);
426431
return notify(report, null);
427432
}
428433

@@ -679,4 +684,59 @@ public static Set<Bugsnag> uncaughtExceptionClients() {
679684
void addOnSession(OnSession onSession) {
680685
sessionTracker.addOnSession(onSession);
681686
}
687+
688+
/**
689+
* Add a feature flag with the specified name and variant.
690+
* If the name already exists, the variant will be updated.
691+
*
692+
* @param name the feature flag name
693+
* @param variant the feature flag variant (can be null)
694+
*/
695+
public void addFeatureFlag(String name, String variant) {
696+
featureFlagStore.addFeatureFlag(name, variant);
697+
}
698+
699+
/**
700+
* Add a feature flag with the specified name and no variant.
701+
*
702+
* @param name the feature flag name
703+
*/
704+
public void addFeatureFlag(String name) {
705+
addFeatureFlag(name, null);
706+
}
707+
708+
/**
709+
* Add multiple feature flags.
710+
* If any names already exist, their variants will be updated.
711+
*
712+
* @param featureFlags the feature flags to add
713+
*/
714+
public void addFeatureFlags(Collection<FeatureFlag> featureFlags) {
715+
featureFlagStore.addFeatureFlags(featureFlags);
716+
}
717+
718+
/**
719+
* Remove the feature flag with the specified name.
720+
*
721+
* @param name the feature flag name to remove
722+
*/
723+
public void clearFeatureFlag(String name) {
724+
featureFlagStore.clearFeatureFlag(name);
725+
}
726+
727+
/**
728+
* Remove all feature flags.
729+
*/
730+
public void clearFeatureFlags() {
731+
featureFlagStore.clearFeatureFlags();
732+
}
733+
734+
/**
735+
* Get a copy of the feature flag store.
736+
*
737+
* @return a copy of the feature flag store
738+
*/
739+
FeatureFlagStore copyFeatureFlagStore() {
740+
return featureFlagStore.copy();
741+
}
682742
}

bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.bugsnag.callbacks.Callback;
44
import com.bugsnag.delivery.Delivery;
55
import com.bugsnag.logback.BugsnagMarker;
6+
import com.bugsnag.logback.LogbackFeatureFlag;
67
import com.bugsnag.logback.LogbackMetadata;
78
import com.bugsnag.logback.LogbackMetadataKey;
89
import com.bugsnag.logback.LogbackMetadataTab;
@@ -74,9 +75,11 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
7475

7576
/** Application version. */
7677
private String appVersion;
77-
7878
private List<LogbackMetadata> globalMetadata = new ArrayList<LogbackMetadata>();
7979

80+
/** Feature flags configured via logback.xml. */
81+
private List<LogbackFeatureFlag> featureFlags = new ArrayList<LogbackFeatureFlag>();
82+
8083
/** Bugsnag client. */
8184
private Bugsnag bugsnag = null;
8285

@@ -271,6 +274,11 @@ private Bugsnag createBugsnag() {
271274
bugsnag.setProjectPackages(projectPackages.toArray(new String[0]));
272275
bugsnag.setSendThreads(sendThreads);
273276

277+
// Add feature flags
278+
for (LogbackFeatureFlag flag : featureFlags) {
279+
bugsnag.addFeatureFlag(flag.getName(), flag.getVariant());
280+
}
281+
274282
// Add a callback to put global metadata on every report
275283
bugsnag.addCallback(new Callback() {
276284
@Override
@@ -592,4 +600,70 @@ private boolean isExcludedLogger(String loggerName) {
592600
}
593601
return false;
594602
}
603+
604+
/**
605+
* Add a feature flag with a name and variant.
606+
* This is typically configured via logback.xml.
607+
*
608+
* @param name the feature flag name
609+
* @param variant the feature flag variant (can be null)
610+
*/
611+
public void addFeatureFlag(String name, String variant) {
612+
LogbackFeatureFlag flag = new LogbackFeatureFlag();
613+
flag.setName(name);
614+
flag.setVariant(variant);
615+
featureFlags.add(flag);
616+
617+
if (bugsnag != null) {
618+
bugsnag.addFeatureFlag(name, variant);
619+
}
620+
}
621+
622+
/**
623+
* Add a feature flag with just a name (no variant).
624+
* This is typically configured via logback.xml.
625+
*
626+
* @param name the feature flag name
627+
*/
628+
public void addFeatureFlag(String name) {
629+
addFeatureFlag(name, null);
630+
}
631+
632+
/**
633+
* Add a feature flag from logback.xml configuration.
634+
* Internal use only - should only be used via the logback.xml file.
635+
*
636+
* @param flag the feature flag to add
637+
*/
638+
public void setFeatureFlag(LogbackFeatureFlag flag) {
639+
featureFlags.add(flag);
640+
641+
if (bugsnag != null) {
642+
bugsnag.addFeatureFlag(flag.getName(), flag.getVariant());
643+
}
644+
}
645+
646+
/**
647+
* Clear a feature flag by name.
648+
*
649+
* @param name the feature flag name to remove
650+
*/
651+
public void clearFeatureFlag(String name) {
652+
featureFlags.removeIf(flag -> flag.getName() != null && flag.getName().equals(name));
653+
654+
if (bugsnag != null) {
655+
bugsnag.clearFeatureFlag(name);
656+
}
657+
}
658+
659+
/**
660+
* Clear all feature flags.
661+
*/
662+
public void clearFeatureFlags() {
663+
featureFlags.clear();
664+
665+
if (bugsnag != null) {
666+
bugsnag.clearFeatureFlags();
667+
}
668+
}
595669
}

bugsnag/src/main/java/com/bugsnag/Configuration.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class Configuration {
4848
Collection<Callback> callbacks = new ConcurrentLinkedQueue<Callback>();
4949
private final AtomicBoolean autoCaptureSessions = new AtomicBoolean(true);
5050
private final AtomicBoolean sendUncaughtExceptions = new AtomicBoolean(true);
51+
private final FeatureFlagStore featureFlagStore = new FeatureFlagStore();
5152

5253
Configuration(String apiKey) {
5354
this.apiKey = apiKey;
@@ -299,4 +300,59 @@ public Serializer getSerializer() {
299300
public void setSerializer(Serializer serializer) {
300301
this.serializer = serializer;
301302
}
303+
304+
/**
305+
* Add a feature flag with the specified name and variant.
306+
* If the name already exists, the variant will be updated.
307+
*
308+
* @param name the feature flag name
309+
* @param variant the feature flag variant (can be null)
310+
*/
311+
public void addFeatureFlag(String name, String variant) {
312+
featureFlagStore.addFeatureFlag(name, variant);
313+
}
314+
315+
/**
316+
* Add a feature flag with the specified name and no variant.
317+
*
318+
* @param name the feature flag name
319+
*/
320+
public void addFeatureFlag(String name) {
321+
addFeatureFlag(name, null);
322+
}
323+
324+
/**
325+
* Add multiple feature flags.
326+
* If any names already exist, their variants will be updated.
327+
*
328+
* @param featureFlags the feature flags to add
329+
*/
330+
public void addFeatureFlags(Collection<FeatureFlag> featureFlags) {
331+
featureFlagStore.addFeatureFlags(featureFlags);
332+
}
333+
334+
/**
335+
* Remove the feature flag with the specified name.
336+
*
337+
* @param name the feature flag name to remove
338+
*/
339+
public void clearFeatureFlag(String name) {
340+
featureFlagStore.clearFeatureFlag(name);
341+
}
342+
343+
/**
344+
* Remove all feature flags.
345+
*/
346+
public void clearFeatureFlags() {
347+
featureFlagStore.clearFeatureFlags();
348+
}
349+
350+
/**
351+
* Get a copy of the feature flag store.
352+
*
353+
* @return a copy of the feature flag store
354+
*/
355+
FeatureFlagStore copyFeatureFlagStore() {
356+
return featureFlagStore.copy();
357+
}
302358
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.bugsnag;
2+
3+
import com.bugsnag.serialization.Expose;
4+
5+
import java.util.Objects;
6+
7+
/**
8+
* Represents a feature flag with a name and optional variant.
9+
* Feature flags can be used to annotate events with information about
10+
* active experiments or A/B tests.
11+
*/
12+
public class FeatureFlag {
13+
private final String name;
14+
private final String variant;
15+
16+
/**
17+
* Create a feature flag with a name and variant.
18+
*
19+
* @param name the name of the feature flag
20+
* @param variant the variant of the feature flag (can be null)
21+
*/
22+
private FeatureFlag(String name, String variant) {
23+
if (name == null || name.isEmpty()) {
24+
throw new IllegalArgumentException("Feature flag name cannot be null or empty");
25+
}
26+
this.name = name;
27+
this.variant = variant;
28+
}
29+
30+
/**
31+
* Get the name of the feature flag.
32+
*
33+
* @return the feature flag name
34+
*/
35+
@Expose
36+
public String getName() {
37+
return name;
38+
}
39+
40+
/**
41+
* Get the variant of the feature flag.
42+
*
43+
* @return the feature flag variant, or null if not set
44+
*/
45+
@Expose
46+
public String getVariant() {
47+
return variant;
48+
}
49+
50+
@Override
51+
public boolean equals(Object obj) {
52+
if (this == obj) {
53+
return true;
54+
}
55+
if (obj == null || getClass() != obj.getClass()) {
56+
return false;
57+
}
58+
FeatureFlag that = (FeatureFlag) obj;
59+
return Objects.equals(name, that.name) && Objects.equals(variant, that.variant);
60+
}
61+
62+
@Override
63+
public int hashCode() {
64+
return Objects.hash(name, variant);
65+
}
66+
67+
@Override
68+
public String toString() {
69+
return "FeatureFlag{name='" + name + "', variant='" + variant + "'}";
70+
}
71+
72+
/**
73+
* Create a feature flag with a name and no vairant.
74+
*
75+
* @param name the name of the feature flag
76+
*/
77+
public static FeatureFlag of(String name) {
78+
if (name == null || name.isEmpty()) {
79+
return null;
80+
}
81+
82+
return new FeatureFlag(name, null);
83+
}
84+
85+
/**
86+
* Create a feature flag with a name and variant.
87+
*
88+
* @param name the name of the feature flag
89+
* @param variant the variant of the feature flag (can be null)
90+
*/
91+
public static FeatureFlag of(String name, String variant) {
92+
if (name == null || name.isEmpty()) {
93+
return null;
94+
}
95+
96+
return new FeatureFlag(name, variant);
97+
}
98+
}

0 commit comments

Comments
 (0)