Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions bugsnag/src/main/java/com/bugsnag/Bugsnag.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Metadata> THREAD_METADATA = new ThreadLocal<Metadata>() {
@Override
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -679,4 +684,59 @@ public static Set<Bugsnag> 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<FeatureFlag> 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();
}
}
76 changes: 75 additions & 1 deletion bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,9 +75,11 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

/** Application version. */
private String appVersion;

private List<LogbackMetadata> globalMetadata = new ArrayList<LogbackMetadata>();

/** Feature flags configured via logback.xml. */
private List<LogbackFeatureFlag> featureFlags = new ArrayList<LogbackFeatureFlag>();

/** Bugsnag client. */
private Bugsnag bugsnag = null;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}
}
56 changes: 56 additions & 0 deletions bugsnag/src/main/java/com/bugsnag/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class Configuration {
Collection<Callback> callbacks = new ConcurrentLinkedQueue<Callback>();
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;
Expand Down Expand Up @@ -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<FeatureFlag> 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();
}
}
80 changes: 80 additions & 0 deletions bugsnag/src/main/java/com/bugsnag/FeatureFlag.java
Original file line number Diff line number Diff line change
@@ -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 + "'}";
}
}
Loading