diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 8914ac15..94af76bd 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -22,6 +22,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; public class Bugsnag implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(Bugsnag.class); @@ -241,10 +242,11 @@ public void setRedactedKeys(String... redactedKeys) { /** * Set which exception classes should be ignored (not sent) by Bugsnag. + * Uses Java regex patterns for matching exception class names. * - * @param discardClasses a list of exception classes to ignore + * @param discardClasses compiled regex patterns to match exception class names */ - public void setDiscardClasses(String... discardClasses) { + public void setDiscardClasses(Pattern... discardClasses) { config.setDiscardClasses(discardClasses); } diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 07f42b56..ad53ba40 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -19,6 +19,7 @@ import java.net.Proxy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -262,7 +263,7 @@ private Bugsnag createBugsnag() { bugsnag.setRedactedKeys(redactedKeys.toArray(new String[0])); } - bugsnag.setDiscardClasses(discardClasses.toArray(new String[0])); + bugsnag.setDiscardClasses(compileDiscardPatterns(discardClasses)); if (!enabledReleaseStages.isEmpty()) { bugsnag.setEnabledReleaseStages(enabledReleaseStages.toArray(new String[0])); @@ -293,6 +294,21 @@ public boolean onError(Report report) { return bugsnag; } + /** + * Compiles a collection of pattern strings into an array of Pattern objects. + * + * @param patternStrings the collection of pattern strings to compile + * @return an array of compiled Pattern objects + */ + private Pattern[] compileDiscardPatterns(Collection patternStrings) { + Pattern[] patterns = new Pattern[patternStrings.size()]; + int idx = 0; + for (String pattern : patternStrings) { + patterns[idx++] = Pattern.compile(pattern); + } + return patterns; + } + /** * Add a callback to execute code before/after every notification to Bugsnag. * @@ -406,24 +422,24 @@ public void setIgnoredClass(String ignoredClass) { } /** - * @see Bugsnag#setDiscardClasses(String...) + * @see Bugsnag#setDiscardClasses(Pattern...) */ public void setDiscardClass(String discardClass) { this.discardClasses.add(discardClass); if (bugsnag != null) { - bugsnag.setDiscardClasses(this.discardClasses.toArray(new String[0])); + bugsnag.setDiscardClasses(compileDiscardPatterns(this.discardClasses)); } } /** - * @see Bugsnag#setDiscardClasses(String...) + * @see Bugsnag#setDiscardClasses(Pattern...) */ public void setDiscardClasses(String discardClasses) { this.discardClasses.addAll(split(discardClasses)); if (bugsnag != null) { - bugsnag.setDiscardClasses(this.discardClasses.toArray(new String[0])); + bugsnag.setDiscardClasses(compileDiscardPatterns(this.discardClasses)); } } diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index 7fd5d536..f6424e2d 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -13,15 +13,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; @SuppressWarnings("visibilitymodifier") public class Configuration { @@ -38,7 +38,8 @@ public class Configuration { private EndpointConfiguration endpoints; private Delivery sessionDelivery; private String[] redactedKeys = new String[] {"password", "secret", "Authorization", "Cookie"}; - private String[] discardClasses; + private Set discardClassRegexPatterns = new HashSet(); + private Set discardClassStringPatterns = new HashSet(); private Set enabledReleaseStages = null; private String[] projectPackages; private String releaseStage; @@ -74,12 +75,16 @@ boolean shouldNotifyForReleaseStage() { } boolean shouldIgnoreClass(String className) { - if (discardClasses == null) { + if (discardClassRegexPatterns == null || discardClassRegexPatterns.isEmpty()) { return false; } - List classes = Arrays.asList(discardClasses); - return classes.contains(className); + for (Pattern pattern : discardClassRegexPatterns) { + if (pattern.matcher(className).matches()) { + return true; + } + } + return false; } void addCallback(Callback callback) { @@ -252,12 +257,50 @@ public void setRedactedKeys(String[] redactedKeys) { this.redactedKeys = redactedKeys; } - public String[] getDiscardClasses() { - return discardClasses; + public Pattern[] getDiscardClasses() { + return discardClassRegexPatterns.toArray(new Pattern[0]); } - public void setDiscardClasses(String[] discardClasses) { - this.discardClasses = discardClasses; + /** + * Set which exception classes should be ignored (not sent) by Bugsnag. + * Uses Java regex patterns for matching exception class names. + * + * @param discardClasses a list of compiled regex patterns to match exception class names + */ + public void setDiscardClasses(Pattern[] discardClasses) { + this.discardClassRegexPatterns.clear(); + this.discardClassStringPatterns.clear(); + if (discardClasses != null) { + for (Pattern pattern : discardClasses) { + if (pattern != null) { + // Store pattern + this.discardClassRegexPatterns.add(pattern); + // Store string representation for serialization + this.discardClassStringPatterns.add(pattern.pattern()); + } + } + } + } + + /** + * Set which exception classes should be ignored (not sent) by Bugsnag. + * Compiles the provided strings as Java regex patterns. + * + * @param discardClasses a list of regex pattern strings to match exception class names + */ + public void setDiscardClassesFromStrings(String[] discardClasses) { + this.discardClassRegexPatterns.clear(); + this.discardClassStringPatterns.clear(); + if (discardClasses != null) { + for (String patternStr : discardClasses) { + if (patternStr != null && !patternStr.isEmpty()) { + // Store original pattern string + this.discardClassStringPatterns.add(patternStr); + // Compile as regex pattern + this.discardClassRegexPatterns.add(Pattern.compile(patternStr)); + } + } + } } public Set getEnabledReleaseStages() { diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index e0e5756a..a6e99b0b 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; /** * Test for the Bugsnag Appender @@ -148,9 +149,19 @@ public void testBugsnagConfig() { assertTrue(redactedKeys.contains("credit_card_number")); assertEquals(2, config.getDiscardClasses().length); - ArrayList discardClasses = new ArrayList(Arrays.asList(config.getDiscardClasses())); - assertTrue(discardClasses.contains("com.example.Custom")); - assertTrue(discardClasses.contains("java.io.IOException")); + Pattern[] discardPatterns = config.getDiscardClasses(); + boolean hasCustom = false; + boolean hasIoException = false; + for (Pattern pattern : discardPatterns) { + if (pattern.pattern().equals("com.example.Custom")) { + hasCustom = true; + } + if (pattern.pattern().equals("java.io.IOException")) { + hasIoException = true; + } + } + assertTrue(hasCustom); + assertTrue(hasIoException); assertEquals(2, config.getEnabledReleaseStages().size()); assertTrue(config.getEnabledReleaseStages().contains("development")); diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index 23d7db97..b2f6525f 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; public class BugsnagTest { @@ -61,13 +62,16 @@ public void testIgnoreClasses() { assertTrue(bugsnag.notify(new RuntimeException())); assertTrue(bugsnag.notify(new TestException())); - // Ignore just RuntimeException - bugsnag.setDiscardClasses(RuntimeException.class.getName()); + // Ignore just RuntimeException (compile pattern for exact match) + bugsnag.setDiscardClasses(Pattern.compile(Pattern.quote(RuntimeException.class.getName()))); assertFalse(bugsnag.notify(new RuntimeException())); assertTrue(bugsnag.notify(new TestException())); - // Ignore both - bugsnag.setDiscardClasses(RuntimeException.class.getName(), TestException.class.getName()); + // Ignore both (compile patterns for exact matches) + bugsnag.setDiscardClasses( + Pattern.compile(Pattern.quote(RuntimeException.class.getName())), + Pattern.compile(Pattern.quote(TestException.class.getName())) + ); assertFalse(bugsnag.notify(new RuntimeException())); assertFalse(bugsnag.notify(new TestException())); } diff --git a/bugsnag/src/test/java/com/bugsnag/ConfigurationDiscardClassesTest.java b/bugsnag/src/test/java/com/bugsnag/ConfigurationDiscardClassesTest.java new file mode 100644 index 00000000..58bc1583 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationDiscardClassesTest.java @@ -0,0 +1,120 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.util.regex.Pattern; + +/** + * Test for Configuration.shouldIgnoreClass with regex pattern matching + */ +public class ConfigurationDiscardClassesTest { + + private Configuration config; + + @Before + public void setUp() { + config = new Configuration("test-api-key"); + } + + @Test + public void testExactMatch() { + config.setDiscardClasses(new Pattern[] {Pattern.compile("com.example.CustomException")}); + + assertTrue(config.shouldIgnoreClass("com.example.CustomException")); + assertFalse(config.shouldIgnoreClass("com.example.OtherException")); + } + + @Test + public void testWildcardMatch() { + config.setDiscardClasses(new Pattern[] {Pattern.compile("com\\.example\\..*")}); + + assertTrue(config.shouldIgnoreClass("com.example.CustomException")); + assertTrue(config.shouldIgnoreClass("com.example.OtherException")); + assertTrue(config.shouldIgnoreClass("com.example.")); + assertFalse(config.shouldIgnoreClass("com.other.Exception")); + } + + @Test + public void testMultipleWildcards() { + config.setDiscardClasses(new Pattern[] {Pattern.compile("com\\..*\\.Exception")}); + + assertTrue(config.shouldIgnoreClass("com.example.Exception")); + assertTrue(config.shouldIgnoreClass("com.other.Exception")); + assertFalse(config.shouldIgnoreClass("com.example.CustomException")); + } + + @Test + public void testQuestionMarkWildcard() { + config.setDiscardClasses(new Pattern[] {Pattern.compile("com\\.example\\.Exception.")}); + + assertTrue(config.shouldIgnoreClass("com.example.Exception1")); + assertTrue(config.shouldIgnoreClass("com.example.ExceptionX")); + assertFalse(config.shouldIgnoreClass("com.example.Exception")); + assertFalse(config.shouldIgnoreClass("com.example.Exception12")); + } + + @Test + public void testMultiplePatterns() { + config.setDiscardClasses(new Pattern[] { + Pattern.compile("java\\.io\\..*"), + Pattern.compile("com\\.example\\.CustomException"), + Pattern.compile("org\\..*\\.SpecialException") + }); + + assertTrue(config.shouldIgnoreClass("java.io.IOException")); + assertTrue(config.shouldIgnoreClass("java.io.FileNotFoundException")); + assertTrue(config.shouldIgnoreClass("com.example.CustomException")); + assertTrue(config.shouldIgnoreClass("org.apache.SpecialException")); + assertTrue(config.shouldIgnoreClass("org.springframework.SpecialException")); + assertFalse(config.shouldIgnoreClass("com.example.OtherException")); + } + + @Test + public void testGetDiscardClassesReturnsOriginalPatterns() { + Pattern[] patterns = new Pattern[] { + Pattern.compile("com\\.example\\..*"), + Pattern.compile("java\\.io\\.IOException") + }; + config.setDiscardClasses(patterns); + + Pattern[] retrieved = config.getDiscardClasses(); + assertEquals(2, retrieved.length); + + // Check that patterns are returned + boolean hasWildcard = false; + boolean hasExact = false; + for (Pattern pattern : retrieved) { + if (pattern.pattern().equals("com\\.example\\..*")) { + hasWildcard = true; + } + if (pattern.pattern().equals("java\\.io\\.IOException")) { + hasExact = true; + } + } + assertTrue(hasWildcard); + assertTrue(hasExact); + } + + @Test + public void testEmptyAndNullPatterns() { + config.setDiscardClasses(new Pattern[] {}); + assertFalse(config.shouldIgnoreClass("com.example.Exception")); + + config.setDiscardClasses(null); + assertFalse(config.shouldIgnoreClass("com.example.Exception")); + } + + @Test + public void testSpecialCharactersAreEscaped() { + // In regex, $ is a special character (end of line), so it needs to be escaped + config.setDiscardClasses(new Pattern[] {Pattern.compile("com\\.example\\.Exception\\$Inner")}); + + assertTrue(config.shouldIgnoreClass("com.example.Exception$Inner")); + assertFalse(config.shouldIgnoreClass("com.example.ExceptionXInner")); + } +} diff --git a/features/fixtures/logback/ignored_class_wildcard_config.xml b/features/fixtures/logback/ignored_class_wildcard_config.xml new file mode 100644 index 00000000..12cbaa70 --- /dev/null +++ b/features/fixtures/logback/ignored_class_wildcard_config.xml @@ -0,0 +1,18 @@ + + + + + + a35a2a72bd230ac0aa0f52715bbdc6aa + production + 1.0.0 + + java\.lang\..* + + http://localhost:9339/notify + + + + + + diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionScenario.java index ef0dfc3d..27178676 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionScenario.java @@ -2,6 +2,8 @@ import com.bugsnag.Bugsnag; +import java.util.regex.Pattern; + /** * Attempts to send an ignored handled exception to Bugsnag, which should not result * in any operation. @@ -15,7 +17,7 @@ public IgnoredExceptionScenario(Bugsnag bugsnag) { @Override public void run() { - bugsnag.setDiscardClasses("java.lang.RuntimeException"); + bugsnag.setDiscardClasses(Pattern.compile("java.lang.RuntimeException")); bugsnag.notify(new RuntimeException("Should never appear")); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionWildcardScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionWildcardScenario.java new file mode 100644 index 00000000..810f3131 --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/IgnoredExceptionWildcardScenario.java @@ -0,0 +1,34 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +import java.util.regex.Pattern; + +/** + * Attempts to send ignored handled exceptions using regex patterns to Bugsnag, + * which should not result in any operation. + */ +public class IgnoredExceptionWildcardScenario extends Scenario { + + public IgnoredExceptionWildcardScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + // Use regex pattern to ignore all java.lang exceptions + bugsnag.setDiscardClasses(Pattern.compile("java\\.lang\\..*")); + + // These should all be ignored due to the regex pattern + bugsnag.notify(new RuntimeException("Should never appear")); + bugsnag.notify(new IllegalArgumentException("Should never appear")); + bugsnag.notify(new IllegalStateException("Should never appear")); + + // This is also ignored due to the regex pattern + try { + throw new NullPointerException("Should never appear"); + } catch (Exception e) { + bugsnag.notify(e); + } + } +} diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleWildcardPatternsScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleWildcardPatternsScenario.java new file mode 100644 index 00000000..aa4d15e3 --- /dev/null +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MultipleWildcardPatternsScenario.java @@ -0,0 +1,34 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; + +import java.util.regex.Pattern; + +/** + * Tests multiple regex patterns working together. + */ +public class MultipleWildcardPatternsScenario extends Scenario { + + public MultipleWildcardPatternsScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + // Set multiple regex patterns: matching specific packages and classes + bugsnag.setDiscardClasses( + Pattern.compile("java\\.io\\..*"), // All java.io exceptions + Pattern.compile("java\\.lang\\.IllegalStateException"), // Exact match + Pattern.compile("java\\.lang\\.Illegal.*") // All IllegalXException classes + ); + + // These should all be ignored + bugsnag.notify(new java.io.IOException("Should be ignored - java\\.io\\..*")); + bugsnag.notify(new java.io.FileNotFoundException("Should be ignored - java\\.io\\..*")); + bugsnag.notify(new IllegalStateException("Should be ignored - exact match")); + bugsnag.notify(new IllegalArgumentException("Should be ignored - java\\.lang\\.Illegal.*")); + + // This should be sent (not matching any pattern) + bugsnag.notify(new RuntimeException("Should be sent")); + } +} diff --git a/features/ignored_reports_wildcard.feature b/features/ignored_reports_wildcard.feature new file mode 100644 index 00000000..d35b87b3 --- /dev/null +++ b/features/ignored_reports_wildcard.feature @@ -0,0 +1,17 @@ +Feature: Reports are ignored with wildcard patterns + +Scenario: Exception classname ignored with wildcard in plain Java app + When I run "IgnoredExceptionWildcardScenario" with the defaults + Then I should receive no errors + +Scenario: Exception classname ignored with wildcard in spring boot app + When I run spring boot "IgnoredExceptionWildcardScenario" with the defaults + Then I should receive no errors + +Scenario: Exception classname ignored with wildcard in plain spring app + When I run plain Spring "IgnoredExceptionWildcardScenario" with the defaults + Then I should receive no errors + +Scenario: Test logback appender with wildcard pattern for ignored error class + When I run "LogbackScenario" with logback config "ignored_class_wildcard_config.xml" + Then I should receive no errors diff --git a/features/multiple_wildcard_patterns.feature b/features/multiple_wildcard_patterns.feature new file mode 100644 index 00000000..49f9b400 --- /dev/null +++ b/features/multiple_wildcard_patterns.feature @@ -0,0 +1,7 @@ +Feature: Multiple wildcard patterns for ignoring reports + +Scenario: Multiple wildcard patterns in plain Java app + When I run "MultipleWildcardPatternsScenario" with the defaults + And I wait to receive an error + And the exception "errorClass" equals "java.lang.RuntimeException" + And the exception "message" equals "Should be sent"