From 5af6abe1c5828e7d1f7e89aaab48cfa51877a613 Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Tue, 21 Oct 2025 13:47:17 +0200 Subject: [PATCH 01/10] uplift part 1: remove javax support, update java to v17 and update gradle to v8.10.2 (#233) --- .buildkite/pipeline.yml | 26 -- bugsnag-spring/build.gradle | 207 ++----------- bugsnag-spring/javax/build.gradle | 41 --- .../javax/src/test/resources/logback.xml | 13 - .../com/bugsnag/BugsnagImportSelector.java | 57 ---- .../bugsnag/ScheduledTaskConfiguration.java | 119 -------- .../BugsnagJavaxMvcExceptionHandler.java | 49 --- .../com/bugsnag/JavaxMvcConfiguration.java | 35 --- .../bugsnag/SpringBootJavaxConfiguration.java | 33 -- .../bugsnag/ScheduledTaskBeanLocatorTest.java | 81 ----- .../ScheduledTaskConfigurationTest.java | 148 --------- .../java/com/bugsnag/SpringAsyncTest.java | 90 ------ .../java/com/bugsnag/SpringMvcTest.java | 282 ------------------ .../com/bugsnag/SpringScheduledTaskTest.java | 95 ------ .../testapp/springboot/AsyncService.java | 19 -- .../testapp/springboot/TestConfiguration.java | 50 ---- .../testapp/springboot/TestController.java | 75 ----- .../springboot/TestSpringBootApplication.java | 15 - .../bugsnag/BugsnagAsyncExceptionHandler.java | 0 .../BugsnagJakartaMvcExceptionHandler.java | 0 .../BugsnagScheduledTaskExceptionHandler.java | 0 .../bugsnag/BugsnagSpringConfiguration.java | 2 +- .../com/bugsnag/ExceptionClassCallback.java | 0 .../com/bugsnag/JakartaMvcConfiguration.java | 0 .../com/bugsnag/ScheduledTaskBeanLocator.java | 0 .../bugsnag/ScheduledTaskConfiguration.java | 197 ++++++++++++ .../com/bugsnag/SpringBootConfiguration.java | 0 .../SpringBootJakartaConfiguration.java | 5 +- .../bugsnag/SpringBootLoadedCondition.java | 0 .../bugsnag/SpringWebMvcLoadedCondition.java | 0 .../java/com/bugsnag/NotifierTest.java | 0 .../bugsnag/ScheduledTaskBeanLocatorTest.java | 0 .../ScheduledTaskConfigurationTest.java | 0 .../java/com/bugsnag/SpringAsyncTest.java | 0 .../java/com/bugsnag/SpringMvcTest.java | 0 .../com/bugsnag/SpringScheduledTaskTest.java | 0 .../java/com/bugsnag/TestUtils.java | 0 .../testapp/springboot/AsyncService.java | 0 .../testapp/springboot/TestConfiguration.java | 0 .../testapp/springboot/TestController.java | 0 .../springboot/TestSpringBootApplication.java | 0 bugsnag/build.gradle | 20 +- .../main/java/com/bugsnag/Configuration.java | 5 - .../callbacks/JavaxServletCallback.java | 88 ------ .../BugsnagServletContainerInitializer.java | 9 - .../BugsnagServletRequestListener.java | 9 - .../BugsnagServletContainerInitializer.java | 13 - .../javax/BugsnagServletRequestListener.java | 42 --- .../com/bugsnag/JavaxServletCallbackTest.java | 145 --------- build.gradle | 6 + common.gradle | 1 - examples/servlet-javax/README.md | 21 -- examples/servlet-javax/build.gradle | 29 -- .../bugsnag/example/servlet/ErrorHandler.java | 31 -- .../example/servlet/ExampleServlet.java | 44 --- .../src/main/webapp/WEB-INF/web.xml | 30 -- features/fixtures/mazerunner/build.gradle | 13 +- .../mazerunnerplainspring/build.gradle | 30 -- .../gradle/wrapper/gradle-wrapper.jar | Bin 54731 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - .../fixtures/mazerunnerplainspring/gradlew | 172 ----------- .../mazerunnerplainspring/gradlew.bat | 84 ------ .../AppConfig.java | 22 -- .../AppInitializer.java | 22 -- .../AsyncMethodService.java | 29 -- .../BugsnagAsyncConfig.java | 19 -- .../BugsnagConfig.java | 34 --- .../ScheduledTaskService.java | 29 -- .../TestRestController.java | 99 ------ .../mazerunnerplainspring6/build.gradle | 11 +- .../mazerunnerspringboot/build.gradle | 44 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 54731 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - .../fixtures/mazerunnerspringboot/gradlew | 172 ----------- .../fixtures/mazerunnerspringboot/gradlew.bat | 84 ------ .../scenarios/AsyncMethodScenario.java | 34 --- .../scenarios/AsyncNotifyScenario.java | 34 --- .../scenarios/AutoSessionScenario.java | 38 --- .../scenarios/RestControllerScenario.java | 29 -- .../ScheduledTaskExecutorScenario.java | 36 --- .../scenarios/ScheduledTaskScenario.java | 27 -- .../mazerunnerspringboot/Application.java | 18 -- .../AsyncMethodService.java | 39 --- .../BugsnagAsyncConfig.java | 21 -- .../bugsnag/mazerunnerspringboot/Config.java | 28 -- .../ScheduledTaskConfig.java | 44 --- .../ScheduledTaskExecutorService.java | 41 --- .../ScheduledTaskService.java | 33 -- .../mazerunnerspringboot/TestCaseRunner.java | 66 ---- .../TestRestController.java | 71 ----- .../src/main/resources/application.properties | 1 - .../mazerunnerspringboot3/build.gradle | 10 +- .../ScheduledTaskConfig.java | 16 +- .../ScheduledTaskService.java | 4 + features/fixtures/scenarios/build.gradle | 6 +- features/fixtures/settings.gradle | 12 +- features/project_package.feature | 8 +- features/scripts/build-plain-spring-app.sh | 9 +- features/scripts/run-java-spring-boot-app.sh | 7 +- gradle/wrapper/gradle-wrapper.properties | 2 +- release.gradle | 101 +++++-- scripts/build-test-repository.sh | 13 +- settings.gradle | 1 - 103 files changed, 379 insertions(+), 3378 deletions(-) delete mode 100644 bugsnag-spring/javax/build.gradle delete mode 100644 bugsnag-spring/javax/src/test/resources/logback.xml delete mode 100644 bugsnag-spring/src/common/java/com/bugsnag/BugsnagImportSelector.java delete mode 100644 bugsnag-spring/src/common/java/com/bugsnag/ScheduledTaskConfiguration.java delete mode 100644 bugsnag-spring/src/javax/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java delete mode 100644 bugsnag-spring/src/javax/java/com/bugsnag/JavaxMvcConfiguration.java delete mode 100644 bugsnag-spring/src/javax/java/com/bugsnag/SpringBootJavaxConfiguration.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskConfigurationTest.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringAsyncTest.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringMvcTest.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringScheduledTaskTest.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/AsyncService.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestConfiguration.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestController.java delete mode 100644 bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java rename bugsnag-spring/src/{common => main}/java/com/bugsnag/BugsnagAsyncExceptionHandler.java (100%) rename bugsnag-spring/src/{jakarta => main}/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java (100%) rename bugsnag-spring/src/{common => main}/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java (100%) rename bugsnag-spring/src/{common => main}/java/com/bugsnag/BugsnagSpringConfiguration.java (96%) rename bugsnag-spring/src/{common => main}/java/com/bugsnag/ExceptionClassCallback.java (100%) rename bugsnag-spring/src/{jakarta => main}/java/com/bugsnag/JakartaMvcConfiguration.java (100%) rename bugsnag-spring/src/{common => main}/java/com/bugsnag/ScheduledTaskBeanLocator.java (100%) create mode 100644 bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java rename bugsnag-spring/src/{common => main}/java/com/bugsnag/SpringBootConfiguration.java (100%) rename bugsnag-spring/src/{jakarta => main}/java/com/bugsnag/SpringBootJakartaConfiguration.java (78%) rename bugsnag-spring/src/{common => main}/java/com/bugsnag/SpringBootLoadedCondition.java (100%) rename bugsnag-spring/src/{common => main}/java/com/bugsnag/SpringWebMvcLoadedCondition.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/NotifierTest.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/ScheduledTaskConfigurationTest.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/SpringAsyncTest.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/SpringMvcTest.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/SpringScheduledTaskTest.java (100%) rename bugsnag-spring/src/{commonTest => test}/java/com/bugsnag/TestUtils.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/testapp/springboot/AsyncService.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/testapp/springboot/TestConfiguration.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/testapp/springboot/TestController.java (100%) rename bugsnag-spring/src/{jakartaTest => test}/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java (100%) delete mode 100644 bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java delete mode 100644 bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java delete mode 100644 bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java delete mode 100644 bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java delete mode 100644 bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java delete mode 100644 bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java delete mode 100644 examples/servlet-javax/README.md delete mode 100644 examples/servlet-javax/build.gradle delete mode 100644 examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java delete mode 100644 examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java delete mode 100644 examples/servlet-javax/src/main/webapp/WEB-INF/web.xml delete mode 100644 features/fixtures/mazerunnerplainspring/build.gradle delete mode 100644 features/fixtures/mazerunnerplainspring/gradle/wrapper/gradle-wrapper.jar delete mode 100644 features/fixtures/mazerunnerplainspring/gradle/wrapper/gradle-wrapper.properties delete mode 100755 features/fixtures/mazerunnerplainspring/gradlew delete mode 100644 features/fixtures/mazerunnerplainspring/gradlew.bat delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java delete mode 100644 features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java delete mode 100644 features/fixtures/mazerunnerspringboot/build.gradle delete mode 100644 features/fixtures/mazerunnerspringboot/gradle/wrapper/gradle-wrapper.jar delete mode 100644 features/fixtures/mazerunnerspringboot/gradle/wrapper/gradle-wrapper.properties delete mode 100755 features/fixtures/mazerunnerspringboot/gradlew delete mode 100644 features/fixtures/mazerunnerspringboot/gradlew.bat delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java delete mode 100644 features/fixtures/mazerunnerspringboot/src/main/resources/application.properties diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 75d6dbb2..2842b995 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -22,32 +22,6 @@ steps: run: java-common command: './gradlew check test' - - label: ':docker: Mazerunner java8 tests batch 1' - key: 'java-mazerunner-tests-1' - depends_on: 'java-jvm-build' - timeout_in_minutes: 30 - plugins: - - docker-compose#v3.7.0: - run: java8-mazerunner - - artifacts#v1.9.0: - download: "maven-repository.zip" - command: - - 'features/scripts/assemble-fixtures.sh' - - 'bundle exec maze-runner --exclude=features/[^a-m].*.feature' - - - label: ':docker: Mazerunner java8 tests batch 2' - key: 'java-mazerunner-tests-2' - depends_on: 'java-jvm-build' - timeout_in_minutes: 30 - plugins: - - docker-compose#v3.7.0: - run: java8-mazerunner - - artifacts#v1.9.0: - download: "maven-repository.zip" - command: - - 'features/scripts/assemble-fixtures.sh' - - 'bundle exec maze-runner --exclude=features/[^n-z].*.feature' - - label: ':docker: Mazerunner java17 tests batch 1' key: 'java-mazerunner-tests-3' depends_on: 'java-jvm-build' diff --git a/bugsnag-spring/build.gradle b/bugsnag-spring/build.gradle index 379cc762..863e38b8 100644 --- a/bugsnag-spring/build.gradle +++ b/bugsnag-spring/build.gradle @@ -1,9 +1,6 @@ ext { jakartaSpringVersion = '6.0.0' jakartaSpringBootVersion = '3.0.0' - - javaxSpringVersion = '5.3.20' - javaxSpringBootVersion = '2.5.14' } apply plugin: 'java-library' @@ -15,190 +12,42 @@ repositories { java { withJavadocJar() toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) + languageVersion.set(JavaLanguageVersion.of(17)) } } -// We use 3 custom configurations and matching sourceSets to avoid the standard behaviours which interfere with -// our compilation structure -configurations { - compileCommon - compileJavax - compileJakarta -} - -sourceSets { - common { - java - // common.compileClasspath includes the compileJavax classpath in order to give the common classes access - // to Spring without confusing the dependencies, including it here ensures that any use of javax.* packages - // in src/common will result in test failures (although not compile failures) - // we need the javax.* packages included here so that the compiler can type-check all the way up the hierarchy - // for things like "ServletRequestBindingException extends ServletException" - compileClasspath += configurations.compileCommon + configurations.compileJavax - } - javax { - java - compileClasspath += configurations.compileJavax + common.output + configurations.compileCommon - } - jakarta { - java - compileClasspath += configurations.compileJakarta + common.output + configurations.compileCommon - } -} - -// test sourceSets -sourceSets { - commonTest { - java - } - javaxTest { - java - compileClasspath += javax.output + javax.compileClasspath + commonTest.output + commonTest.runtimeClasspath + common.output - runtimeClasspath = compileClasspath - } - jakartaTest { - java - compileClasspath += jakarta.output + jakarta.compileClasspath + commonTest.output + commonTest.runtimeClasspath + common.output - runtimeClasspath = compileClasspath - } -} tasks.register('sourceJar', Jar).configure { - from( - sourceSets.common.allJava, - sourceSets.javax.allJava, - sourceSets.jakarta.allJava - ) + from sourceSets.main.allJava } // do not move this higher - sourceJar must be registered before we apply common.gradle apply from: '../common.gradle' -compileJava.dependsOn(compileCommonJava, compileJavaxJava, compileJakartaJava) - -// Separated Javax / Jakarta tests ------------------------------------------------------------------------------------- - -compileJakartaJava { - // set the compiler for the `jakarta` sourceSet to Java17 - javaCompiler.set( - javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) - } - ) -} - -compileJakartaTestJava { - // set the compiler for the `jakartaTest` sourceSet to Java17 - javaCompiler.set( - javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) - } - ) -} - -testClasses.dependsOn(javaxTestClasses, jakartaTestClasses) - -tasks.register('testJakarta', Test) { - testClassesDirs = sourceSets.jakartaTest.output.classesDirs - classpath = sourceSets.jakartaTest.output.classesDirs + sourceSets.jakartaTest.runtimeClasspath - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(17) - } - ) - - dependsOn(jakartaTestClasses) -} - -tasks.register('testJavax', Test) { - testClassesDirs = sourceSets.javaxTest.output.classesDirs - classpath = sourceSets.javaxTest.output.classesDirs + sourceSets.javaxTest.runtimeClasspath - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) - } - ) - - dependsOn(javaxTestClasses) -} - -test.dependsOn(testJakarta, testJavax) - -dependencies { - compileCommon project(':bugsnag') - - compileCommon "ch.qos.logback:logback-core:${logbackVersion}" - compileCommon "org.slf4j:slf4j-api:${slf4jApiVersion}" - - compileJavax "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" - compileJavax "org.springframework:spring-webmvc:${javaxSpringVersion}" - compileJavax "org.springframework.boot:spring-boot:${javaxSpringBootVersion}" - compileJavax "org.springframework:spring-aop:${javaxSpringVersion}" - - compileJakarta "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" - compileJakarta "org.springframework:spring-webmvc:${jakartaSpringVersion}" - compileJakarta "org.springframework.boot:spring-boot:${jakartaSpringBootVersion}" - compileJakarta "org.springframework:spring-aop:${jakartaSpringVersion}" - - - commonTestImplementation project(':bugsnag').sourceSets.test.output - commonTestImplementation project(':bugsnag') - - commonTestImplementation "junit:junit:${junitVersion}" - - commonTestCompileOnly "org.mockito:mockito-core:2.10.0" - - jakartaTestImplementation "org.mockito:mockito-core:${mockitoVersion}" - jakartaTestImplementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" - jakartaTestImplementation "org.springframework.boot:spring-boot-starter-test:${jakartaSpringBootVersion}" - jakartaTestImplementation "org.springframework.boot:spring-boot-starter-web:${jakartaSpringBootVersion}" - jakartaTestImplementation "org.springframework:spring-aop:${jakartaSpringVersion}" - - javaxTestImplementation "org.mockito:mockito-core:2.10.0" - javaxTestImplementation "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" - javaxTestImplementation "org.springframework.boot:spring-boot-starter-test:${javaxSpringBootVersion}" - javaxTestImplementation "org.springframework.boot:spring-boot-starter-web:${javaxSpringBootVersion}" - javaxTestImplementation "org.springframework:spring-aop:${javaxSpringVersion}" -} - dependencies { - // ensure that the published .pom file includes a dependency on com.bugsnag:bugsnag - // this 'api' dependency has no impact on the compilation of this projects source, - // so we keep it in it's own dependencies block - api project(':bugsnag') -} - -// here is where we merge all of the class outputs into the default classes directory doing this as its own step avoids -// circular dependencies between the sourceSets and their output directories (if they all use 'main.output.classesDirs' -// then jakarta+javax both depend on it as well) -tasks.register('mergeClasses', Copy) { - from sourceSets.common.output.classesDirs - from sourceSets.jakarta.output.classesDirs - from sourceSets.javax.output.classesDirs - - into sourceSets.main.output.classesDirs.asPath -} - -classes.dependsOn('mergeClasses') - -if (project.hasProperty('releasing')) { - publishing { - publications { - Publication(MavenPublication) { - // this is vital: we use the version details from the "javax" side of the project - // this ensures that Gradle considers that as the baseline set of required versions - // allowing old projects that still use Java8 and the javax.servlet packages to use - // bugsnag-java without any secondary artifacts - versionMapping { - usage('java-api') { - fromResolutionOf('compileCommon') - } - usage('java-runtime') { - fromResolutionOf('compileCommon') - } - } - } - } - } -} + implementation project(':bugsnag') + + implementation "ch.qos.logback:logback-core:${logbackVersion}" + implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" + + implementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" + implementation "org.springframework:spring-webmvc:${jakartaSpringVersion}" + implementation "org.springframework.boot:spring-boot:${jakartaSpringBootVersion}" + implementation "org.springframework:spring-aop:${jakartaSpringVersion}" + + testImplementation project(':bugsnag').sourceSets.test.output + testImplementation project(':bugsnag') + testImplementation "junit:junit:${junitVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-test:${jakartaSpringBootVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-web:${jakartaSpringBootVersion}" + testImplementation "org.junit.jupiter:junit-jupiter:5.8.2" + testCompileOnly "org.mockito:mockito-core:2.10.0" +} + +/** ---- Publishing config ---- + * Pulls in publishing+signing rules from the shared release.gradle. + * This will create tasks like: + * :bugsnag:publishMavenJavaPublicationToTestRepository + * :bugsnag:publishMavenJavaPublicationToOssrhStagingRepository + */ +apply from: "${rootProject.projectDir}/release.gradle" \ No newline at end of file diff --git a/bugsnag-spring/javax/build.gradle b/bugsnag-spring/javax/build.gradle deleted file mode 100644 index edb7f763..00000000 --- a/bugsnag-spring/javax/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -ext { - springVersion = '5.3.20' - springBootVersion = '2.5.14' -} - -apply plugin: 'java' -apply plugin: 'java-library' - -apply from: '../../common.gradle' - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(8) - } -} - -compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' -} - -repositories { - mavenCentral() -} - -dependencies { - api project(':bugsnag') - testImplementation project(':bugsnag').sourceSets.test.output - - compileOnly "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" - compileOnly "org.springframework:spring-webmvc:${springVersion}" - compileOnly "org.springframework.boot:spring-boot:${springBootVersion}" - compileOnly "ch.qos.logback:logback-core:${logbackVersion}" - compileOnly "org.slf4j:slf4j-api:${slf4jApiVersion}" - - testImplementation "junit:junit:${junitVersion}" - testImplementation "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" - testImplementation "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" - testImplementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" -} \ No newline at end of file diff --git a/bugsnag-spring/javax/src/test/resources/logback.xml b/bugsnag-spring/javax/src/test/resources/logback.xml deleted file mode 100644 index 073f1525..00000000 --- a/bugsnag-spring/javax/src/test/resources/logback.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - apiKey - - - - - - - \ No newline at end of file diff --git a/bugsnag-spring/src/common/java/com/bugsnag/BugsnagImportSelector.java b/bugsnag-spring/src/common/java/com/bugsnag/BugsnagImportSelector.java deleted file mode 100644 index 36f10e1a..00000000 --- a/bugsnag-spring/src/common/java/com/bugsnag/BugsnagImportSelector.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.bugsnag; - -import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.SpringVersion; -import org.springframework.core.type.AnnotationMetadata; - -public class BugsnagImportSelector implements ImportSelector { - - private static final String[] SPRING_JAKARTA_CLASSES = { - "com.bugsnag.SpringBootJakartaConfiguration", - "com.bugsnag.JakartaMvcConfiguration", - "com.bugsnag.ScheduledTaskConfiguration" - }; - - private static final String[] SPRING_JAVAX_CLASSES = { - "com.bugsnag.SpringBootJavaxConfiguration", - "com.bugsnag.JavaxMvcConfiguration", - "com.bugsnag.ScheduledTaskConfiguration" - }; - - @Override - public String[] selectImports(AnnotationMetadata importingClassMetadata) { - if (isSpringJakartaCompatible() && isJava17Compatible()) { - return SPRING_JAKARTA_CLASSES; - } - - return SPRING_JAVAX_CLASSES; - } - - private static boolean isSpringJakartaCompatible() { - return getMajorVersion(SpringVersion.getVersion()) >= 6; - } - - private static boolean isJava17Compatible() { - return getMajorVersion(System.getProperty("java.version")) >= 17; - } - - private static int getMajorVersion(String version) { - if (version == null) { - return 0; - } - int firstDot = version.indexOf("."); - String majorVersion; - - if (firstDot == -1) { - majorVersion = version; - } else { - majorVersion = version.substring(0, firstDot); - } - - try { - return Integer.parseInt(majorVersion); - } catch (NumberFormatException nfe) { - return 0; - } - } -} diff --git a/bugsnag-spring/src/common/java/com/bugsnag/ScheduledTaskConfiguration.java b/bugsnag-spring/src/common/java/com/bugsnag/ScheduledTaskConfiguration.java deleted file mode 100644 index 6c9718a9..00000000 --- a/bugsnag-spring/src/common/java/com/bugsnag/ScheduledTaskConfiguration.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.bugsnag; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.aop.framework.AopProxyUtils; -import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; - -import org.springframework.util.ErrorHandler; - -import java.lang.reflect.Field; -import java.util.concurrent.ScheduledExecutorService; - -/** - * Add configuration for reporting unhandled exceptions for scheduled tasks. - */ -@Configuration -class ScheduledTaskConfiguration implements SchedulingConfigurer { - - private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTaskConfiguration.class); - - @Autowired - private Bugsnag bugsnag; - - @Autowired - private ScheduledTaskBeanLocator beanLocator; - - /** - * Add bugsnag error handling to a task scheduler - */ - @Override - public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { - BugsnagScheduledTaskExceptionHandler bugsnagErrorHandler = - new BugsnagScheduledTaskExceptionHandler(bugsnag); - - // Decision process for finding a TaskScheduler, in order of preference: - // - // 1. use the scheduler from the task registrar - // 2. search for a TaskScheduler bean, by type, then by name - // 3. search for a ScheduledExecutorService bean by type, then by name, - // and wrap it in a TaskScheduler - // 4. create our own TaskScheduler - - TaskScheduler registrarScheduler = taskRegistrar.getScheduler(); - TaskScheduler taskScheduler = registrarScheduler != null - ? registrarScheduler : beanLocator.resolveTaskScheduler(); - - if (taskScheduler != null) { - //check if taskSchedular is a proxy - if (AopUtils.isAopProxy(taskScheduler)) { - //if it's a proxy then get the target class and cast as necessary - Class targetClass = AopProxyUtils.ultimateTargetClass(taskScheduler); - if (TaskScheduler.class.isAssignableFrom(targetClass)) { - taskScheduler = (TaskScheduler) AopProxyUtils.getSingletonTarget(taskScheduler); - } - } - configureExistingTaskScheduler(taskScheduler, bugsnagErrorHandler); - } else { - ScheduledExecutorService executorService = beanLocator.resolveScheduledExecutorService(); - taskScheduler = createNewTaskScheduler(executorService, bugsnagErrorHandler); - taskRegistrar.setScheduler(taskScheduler); - } - } - - private TaskScheduler createNewTaskScheduler( - ScheduledExecutorService executorService, - BugsnagScheduledTaskExceptionHandler errorHandler) { - if (executorService != null) { - // create a task scheduler which delegates to the existing Executor - ConcurrentTaskScheduler scheduler = new ConcurrentTaskScheduler(executorService); - scheduler.setErrorHandler(errorHandler); - return scheduler; - } else { - // If no task scheduler has been defined by the application, create one - // and add the bugsnag error handler. - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setErrorHandler(errorHandler); - scheduler.initialize(); - return scheduler; - } - } - - /** - * If a task scheduler has been defined by the application, get it so that - * bugsnag error handling can be added. - *

- * Reflection is the simplest way to get and set an error handler - * because the error handler setter is only defined in the concrete classes, - * not the TaskScheduler interface. - * - * @param taskScheduler the task scheduler - */ - private void configureExistingTaskScheduler(TaskScheduler taskScheduler, - BugsnagScheduledTaskExceptionHandler errorHandler) { - try { - Field errorHandlerField = - taskScheduler.getClass().getDeclaredField("errorHandler"); - errorHandlerField.setAccessible(true); - Object existingErrorHandler = errorHandlerField.get(taskScheduler); - - // If an error handler has already been defined then make the Bugsnag handler - // call this afterwards - if (existingErrorHandler instanceof ErrorHandler) { - errorHandler.setExistingErrorHandler((ErrorHandler) existingErrorHandler); - } - - // Add the bugsnag error handler to the scheduler. - errorHandlerField.set(taskScheduler, errorHandler); - } catch (Throwable ex) { - LOGGER.warn("Bugsnag scheduled task exception handler could not be configured"); - } - } -} diff --git a/bugsnag-spring/src/javax/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java b/bugsnag-spring/src/javax/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java deleted file mode 100644 index 62cbf9ea..00000000 --- a/bugsnag-spring/src/javax/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.bugsnag; - -import com.bugsnag.HandledState.SeverityReasonType; - -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.web.servlet.HandlerExceptionResolver; -import org.springframework.web.servlet.ModelAndView; - -import java.util.Collections; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Reports uncaught exceptions thrown from handler mapping or execution to Bugsnag - * and then passes the exception to the next handler in the chain. - * - * Set to highest precedence so that it should be called before other exception - * resolvers. - */ -@Order(Ordered.HIGHEST_PRECEDENCE) -class BugsnagJavaxMvcExceptionHandler implements HandlerExceptionResolver { - - private final Bugsnag bugsnag; - - BugsnagJavaxMvcExceptionHandler(final Bugsnag bugsnag) { - this.bugsnag = bugsnag; - } - - @Override - public ModelAndView resolveException(HttpServletRequest request, - HttpServletResponse response, - Object handler, - java.lang.Exception ex) { - - if (bugsnag.getConfig().shouldSendUncaughtExceptions()) { - HandledState handledState = HandledState.newInstance( - SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE, - Collections.singletonMap("framework", "Spring"), - Severity.ERROR, - true); - - bugsnag.notify(ex, handledState, Thread.currentThread()); - } - - // Returning null passes the exception onto the next resolver in the chain. - return null; - } -} diff --git a/bugsnag-spring/src/javax/java/com/bugsnag/JavaxMvcConfiguration.java b/bugsnag-spring/src/javax/java/com/bugsnag/JavaxMvcConfiguration.java deleted file mode 100644 index 8ba8e5e8..00000000 --- a/bugsnag-spring/src/javax/java/com/bugsnag/JavaxMvcConfiguration.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.bugsnag; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; - -/** - * If spring-webmvc is loaded, add configuration for reporting unhandled exceptions. - */ -@Configuration -@Conditional(SpringWebMvcLoadedCondition.class) -class JavaxMvcConfiguration implements InitializingBean { - - @Autowired - private Bugsnag bugsnag; - - /** - * Register an exception resolver to send unhandled reports to Bugsnag - * for uncaught exceptions thrown from request handlers. - */ - @Bean - BugsnagJavaxMvcExceptionHandler bugsnagHandlerExceptionResolver() { - return new BugsnagJavaxMvcExceptionHandler(bugsnag); - } - - /** - * Add a callback to assign specified severities for some Spring exceptions. - */ - @Override - public void afterPropertiesSet() { - bugsnag.addCallback(new ExceptionClassCallback()); - } -} diff --git a/bugsnag-spring/src/javax/java/com/bugsnag/SpringBootJavaxConfiguration.java b/bugsnag-spring/src/javax/java/com/bugsnag/SpringBootJavaxConfiguration.java deleted file mode 100644 index 1f63c81d..00000000 --- a/bugsnag-spring/src/javax/java/com/bugsnag/SpringBootJavaxConfiguration.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.bugsnag; - -import com.bugsnag.servlet.javax.BugsnagServletRequestListener; - -import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; - -import javax.servlet.ServletRequestListener; - -/** - * If spring-boot is loaded, add configuration specific to Spring Boot - */ -@Configuration -@Conditional(SpringBootLoadedCondition.class) -class SpringBootJavaxConfiguration extends SpringBootConfiguration { - - /** - * The {@link com.bugsnag.servlet.javax.BugsnagServletContainerInitializer} does not work for Spring Boot, need to - * register the {@link BugsnagServletRequestListener} using a Spring Boot - * {@link ServletListenerRegistrationBean} instead. This adds session tracking and - * automatic servlet request metadata collection. - */ - @Bean - @Conditional(SpringWebMvcLoadedCondition.class) - ServletListenerRegistrationBean listenerRegistrationBean() { - ServletListenerRegistrationBean srb = - new ServletListenerRegistrationBean(); - srb.setListener(new BugsnagServletRequestListener()); - return srb; - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java deleted file mode 100644 index 74eb856d..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.bugsnag; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.when; - -import com.bugsnag.testapp.springboot.TestSpringBootApplication; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.ApplicationContext; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = TestSpringBootApplication.class) -public class ScheduledTaskBeanLocatorTest { - - @Autowired - private ScheduledTaskBeanLocator beanLocator; - - @MockBean - private ApplicationContext context; - - @Before - public void setUp() { - beanLocator.setApplicationContext(context); - } - - @Test - public void findSchedulerByType() { - ThreadPoolTaskScheduler expected = new ThreadPoolTaskScheduler(); - when(context.getBean(TaskScheduler.class)).thenReturn(expected); - assertEquals(expected, beanLocator.resolveTaskScheduler()); - } - - @Test - public void findSchedulerByName() { - ThreadPoolTaskScheduler expected = new ThreadPoolTaskScheduler(); - Throwable exc = new NoUniqueBeanDefinitionException(TaskScheduler.class); - when(context.getBean(TaskScheduler.class)).thenThrow(exc); - when(context.getBean("taskScheduler", TaskScheduler.class)).thenReturn(expected); - assertEquals(expected, beanLocator.resolveTaskScheduler()); - } - - @Test - public void noTaskSchedulerAvailable() { - assertNull(beanLocator.resolveTaskScheduler()); - } - - @Test - public void findExecutorByType() { - ScheduledExecutorService expected = Executors.newScheduledThreadPool(1); - when(context.getBean(ScheduledExecutorService.class)).thenReturn(expected); - assertEquals(expected, beanLocator.resolveScheduledExecutorService()); - } - - @Test - public void findExecutorByName() { - ScheduledExecutorService expected = Executors.newScheduledThreadPool(4); - Throwable exc = new NoUniqueBeanDefinitionException(ScheduledExecutorService.class); - when(context.getBean(ScheduledExecutorService.class)).thenThrow(exc); - when(context.getBean("taskScheduler", ScheduledExecutorService.class)) - .thenReturn(expected); - assertEquals(expected, beanLocator.resolveScheduledExecutorService()); - } - - @Test - public void noScheduledExecutorAvailable() { - assertNull(beanLocator.resolveScheduledExecutorService()); - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskConfigurationTest.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskConfigurationTest.java deleted file mode 100644 index f619795d..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/ScheduledTaskConfigurationTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.bugsnag; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.when; - -import com.bugsnag.testapp.springboot.TestSpringBootApplication; - -import org.aopalliance.intercept.MethodInterceptor; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.ApplicationContext; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.test.context.junit4.SpringRunner; - -import java.lang.reflect.Field; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = TestSpringBootApplication.class) -public class ScheduledTaskConfigurationTest { - - @Autowired - private ScheduledTaskConfiguration configuration; - - @Mock - private ScheduledTaskRegistrar registrar; - - @Autowired - private ScheduledTaskBeanLocator beanLocator; - - @MockBean - private ApplicationContext context; - - @Before - public void setUp() { - registrar = new ScheduledTaskRegistrar(); - beanLocator.setApplicationContext(context); - } - - @Test - public void existingSchedulerUsed() { - ThreadPoolTaskScheduler expected = new ThreadPoolTaskScheduler(); - registrar.setScheduler(expected); - configuration.configureTasks(registrar); - assertEquals(expected, registrar.getScheduler()); - } - - @Test - public void noSchedulersAvailable() { - configuration.configureTasks(registrar); - assertTrue(registrar.getScheduler() instanceof ThreadPoolTaskScheduler); - } - - @Test - public void findSchedulerByType() throws NoSuchFieldException, IllegalAccessException { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - when(context.getBean(TaskScheduler.class)).thenReturn(scheduler); - - configuration.configureTasks(registrar); - assertNull(registrar.getScheduler()); - Object errorHandler = accessField(scheduler, "errorHandler"); - assertTrue(errorHandler instanceof BugsnagScheduledTaskExceptionHandler); - } - - @Test - public void findSchedulerByName() throws NoSuchFieldException, IllegalAccessException { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - Throwable exc = new NoUniqueBeanDefinitionException(TaskScheduler.class); - when(context.getBean(TaskScheduler.class)).thenThrow(exc); - when(context.getBean("taskScheduler", TaskScheduler.class)).thenReturn(scheduler); - - configuration.configureTasks(registrar); - assertNull(registrar.getScheduler()); - Object errorHandler = accessField(scheduler, "errorHandler"); - assertTrue(errorHandler instanceof BugsnagScheduledTaskExceptionHandler); - } - - @Test - public void findExecutorByType() throws NoSuchFieldException, IllegalAccessException { - ScheduledExecutorService expected = Executors.newScheduledThreadPool(1); - when(context.getBean(ScheduledExecutorService.class)).thenReturn(expected); - - configuration.configureTasks(registrar); - TaskScheduler scheduler = registrar.getScheduler(); - assertTrue(scheduler instanceof ConcurrentTaskScheduler); - assertEquals(expected, accessField(scheduler, "scheduledExecutor")); - } - - @Test - public void findExecutorByName() throws NoSuchFieldException, IllegalAccessException { - ScheduledExecutorService expected = Executors.newScheduledThreadPool(4); - Throwable exc = new NoUniqueBeanDefinitionException(ScheduledExecutorService.class); - when(context.getBean(ScheduledExecutorService.class)).thenThrow(exc); - when(context.getBean("taskScheduler", ScheduledExecutorService.class)) - .thenReturn(expected); - - configuration.configureTasks(registrar); - TaskScheduler scheduler = registrar.getScheduler(); - assertTrue(scheduler instanceof ConcurrentTaskScheduler); - assertEquals(expected, accessField(scheduler, "scheduledExecutor")); - } - - @Test - public void configureTasks_withProxyWrappedRegistrar() throws NoSuchFieldException, IllegalAccessException { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - when(context.getBean(TaskScheduler.class)).thenReturn(scheduler); - TaskScheduler proxyScheduler = createProxy(scheduler); - registrar.setScheduler(proxyScheduler); - Object errorHandler = accessField(scheduler, "errorHandler"); - assertFalse( - errorHandler instanceof BugsnagScheduledTaskExceptionHandler, - "errorHandler should not be BugsnagScheduledTaskExceptionHandler" - ); - configuration.configureTasks(registrar); - errorHandler = accessField(scheduler, "errorHandler"); - assertTrue( - "errorHandler should be BugsnagScheduledTaskExceptionHandler", - errorHandler instanceof BugsnagScheduledTaskExceptionHandler - ); - } - - private TaskScheduler createProxy(TaskScheduler target) { - ProxyFactory factory = new ProxyFactory(target); - factory.addAdvice((MethodInterceptor) invocation -> invocation.proceed()); - return (TaskScheduler) factory.getProxy(); - } - - private Object accessField(Object object, String fieldName) - throws NoSuchFieldException, IllegalAccessException { - Field field = object.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(object); - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringAsyncTest.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringAsyncTest.java deleted file mode 100644 index 1bdfa04b..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringAsyncTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.bugsnag; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -import com.bugsnag.HandledState.SeverityReasonType; -import com.bugsnag.delivery.Delivery; -import com.bugsnag.testapp.springboot.AsyncService; -import com.bugsnag.testapp.springboot.TestSpringBootApplication; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.Collections; - -/** - * Test that a Spring Boot application configured with the - * {@link BugsnagSpringConfiguration} performs as expected. - */ -@RunWith(SpringRunner.class) -@SpringBootTest(classes = TestSpringBootApplication.class) -public class SpringAsyncTest { - - @Autowired - private Bugsnag bugsnag; - - @Autowired - private AsyncService asyncService; - - private Delivery delivery; - - /** - * Initialize test state - */ - @Before - public void setUp() { - delivery = mock(Delivery.class); - bugsnag.setDelivery(delivery); - } - - @Test - public void bugsnagNotifyWhenAsyncVoidReturnTypeException() { - asyncService.throwExceptionVoid(); - - Report report = TestUtils.verifyAndGetReport(delivery); - - // Assert that the exception was detected correctly - assertEquals("Async void test", report.getExceptionMessage()); - assertEquals("java.lang.RuntimeException", report.getExceptionName()); - - // Assert that the severity, severity reason and unhandled values are correct - Assert.assertEquals(Severity.ERROR.getValue(), report.getSeverity()); - assertEquals( - SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), - report.getSeverityReason().getType()); - assertThat( - report.getSeverityReason().getAttributes(), - is(Collections.singletonMap("framework", "Spring"))); - assertTrue(report.getUnhandled()); - } - - @Test - public void bugsnagNotifyWhenAsyncFutureReturnTypeException() { - asyncService.throwExceptionFuture(); - - Report report = TestUtils.verifyAndGetReport(delivery); - - // Assert that the exception was detected correctly - assertEquals("Async future test", report.getExceptionMessage()); - assertEquals("java.lang.RuntimeException", report.getExceptionName()); - - // Assert that the severity, severity reason and unhandled values are correct - assertEquals(Severity.ERROR.getValue(), report.getSeverity()); - assertEquals( - SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), - report.getSeverityReason().getType()); - assertThat( - report.getSeverityReason().getAttributes(), - is(Collections.singletonMap("framework", "Spring"))); - assertTrue(report.getUnhandled()); - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringMvcTest.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringMvcTest.java deleted file mode 100644 index d426ba01..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringMvcTest.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.bugsnag; - -import static com.bugsnag.TestUtils.anyMapOf; -import static com.bugsnag.TestUtils.verifyAndGetReport; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.bugsnag.HandledState.SeverityReasonType; -import com.bugsnag.callbacks.Callback; -import com.bugsnag.delivery.Delivery; -import com.bugsnag.serialization.Serializer; -import com.bugsnag.testapp.springboot.TestSpringBootApplication; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootVersion; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.core.SpringVersion; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.test.context.junit4.SpringRunner; - -import java.lang.reflect.Field; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -/** - * Test that a Spring Boot application configured with the - * {@link BugsnagSpringConfiguration} performs as expected. - */ -@RunWith(SpringRunner.class) -@SpringBootTest( - classes = TestSpringBootApplication.class, - webEnvironment = WebEnvironment.RANDOM_PORT) -public class SpringMvcTest { - - @LocalServerPort - private int randomServerPort; - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private Bugsnag bugsnag; - - private Delivery delivery; - - private long sessionsStartedBeforeTest; - - /** - * Initialize test state - */ - @Before - public void setUp() { - delivery = mock(Delivery.class); - - bugsnag.setDelivery(delivery); - bugsnag.getConfig().setSendUncaughtExceptions(true); - bugsnag.getConfig().setAutoCaptureSessions(true); - - // Cannot reset the session count on the bugsnag bean for each test, so note - // the current session count before the test starts instead. - sessionsStartedBeforeTest = getSessionCount(); - } - - @Test - public void bugsnagNotifyWhenUncaughtControllerException() { - callRuntimeExceptionEndpoint(); - - Report report = verifyAndGetReport(delivery); - - // Assert that the exception was detected correctly - assertEquals("Test", report.getExceptionMessage()); - assertEquals("java.lang.RuntimeException", report.getExceptionName()); - - // Assert that the severity, severity reason and unhandled values are correct - Assert.assertEquals(Severity.ERROR.getValue(), report.getSeverity()); - assertEquals( - SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), - report.getSeverityReason().getType()); - assertThat( - report.getSeverityReason().getAttributes(), - is(Collections.singletonMap("framework", "Spring"))); - assertTrue(report.getUnhandled()); - } - - @Test - public void noBugsnagNotifyWhenSendUncaughtExceptionsFalse() { - bugsnag.getConfig().setSendUncaughtExceptions(false); - - callRuntimeExceptionEndpoint(); - - verifyNoReport(); - } - - @Test - public void bugsnagSessionStartedWhenAutoCaptureSessions() { - callRuntimeExceptionEndpoint(); - - assertSessionsStarted(1); - } - - @Test - public void noBugsnagSessionStartedWhenAutoCaptureSessionsFalse() { - bugsnag.getConfig().setAutoCaptureSessions(false); - - callRuntimeExceptionEndpoint(); - - assertSessionsStarted(0); - } - - @Test - public void requestMetadataSetCorrectly() { - callRuntimeExceptionEndpoint(); - - Report report = verifyAndGetReport(delivery); - - // Check that the context is set to the HTTP method and URI of the endpoint - assertEquals("GET /throw-runtime-exception", report.getContext()); - - // Check that the request metadata is set as expected - @SuppressWarnings(value = "unchecked") Map requestMetadata = - (Map) report.getMetaData().get("request"); - assertEquals("http://localhost:" + randomServerPort + "/throw-runtime-exception", - requestMetadata.get("url")); - assertEquals("GET", requestMetadata.get("method")); - assertEquals("127.0.0.1", requestMetadata.get("clientIp")); - - // Assert that the request params are as expected - @SuppressWarnings(value = "unchecked") Map params = - (Map) requestMetadata.get("params"); - assertEquals("paramVal1", params.get("param1")[0]); - assertEquals("paramVal2", params.get("param2")[0]); - - // Assert that the request headers are as expected, including headers with - // multiple values represented as a comma-separated string. - @SuppressWarnings(value = "unchecked") Map headers = - (Map) requestMetadata.get("headers"); - assertEquals("header1Val1,header1Val2", headers.get("header1")); - assertEquals("header2Val1", headers.get("header2")); - } - - @Test - @SuppressWarnings("unchecked") - public void springVersionSetCorrectly() { - callRuntimeExceptionEndpoint(); - - Report report = verifyAndGetReport(delivery); - - // Check that the Spring version is set as expected - Map deviceMetadata = report.getDevice(); - Map runtimeVersions = - (Map) deviceMetadata.get("runtimeVersions"); - assertEquals(SpringVersion.getVersion(), runtimeVersions.get("springFramework")); - assertEquals(SpringBootVersion.getVersion(), runtimeVersions.get("springBoot")); - } - - @Test - public void unhandledTypeMismatchExceptionSeverityInfo() { - callUnhandledTypeMismatchExceptionEndpoint(); - - Report report = verifyAndGetReport(delivery); - - assertTrue(report.getUnhandled()); - assertEquals("info", report.getSeverity()); - assertEquals("exceptionClass", report.getSeverityReason().getType()); - assertThat(report.getSeverityReason().getAttributes(), - is(Collections.singletonMap("exceptionClass", "TypeMismatchException"))); - } - - @Test - public void unhandledTypeMismatchExceptionCallbackSeverity() - throws IllegalAccessException, NoSuchFieldException { - Report report; - Callback callback = new Callback() { - @Override - public void beforeNotify(Report report) { - report.setSeverity(Severity.WARNING); - } - }; - - try { - bugsnag.addCallback(callback); - - callUnhandledTypeMismatchExceptionEndpoint(); - - report = verifyAndGetReport(delivery); - } finally { - // Remove the callback via reflection so that subsequent tests do not use it - Field callbacksField = Configuration.class.getDeclaredField("callbacks"); - @SuppressWarnings(value = "unchecked") Collection callbacks = - (Collection) callbacksField.get(bugsnag.getConfig()); - callbacks.remove(callback); - } - - assertTrue(report.getUnhandled()); - assertEquals("warning", report.getSeverity()); - assertEquals("userCallbackSetSeverity", report.getSeverityReason().getType()); - } - - @Test - public void handledTypeMismatchExceptionUserSeverity() { - callHandledTypeMismatchExceptionUserSeverityEndpoint(); - - Report report = verifyAndGetReport(delivery); - - assertFalse(report.getUnhandled()); - assertEquals("warning", report.getSeverity()); - assertEquals("userSpecifiedSeverity", report.getSeverityReason().getType()); - assertThat(report.getSeverityReason().getAttributes(), is(Collections.EMPTY_MAP)); - } - - @Test - public void handledTypeMismatchExceptionCallbackSeverity() { - callHandledTypeMismatchExceptionCallbackSeverityEndpoint(); - - Report report = verifyAndGetReport(delivery); - - assertFalse(report.getUnhandled()); - assertEquals("warning", report.getSeverity()); - assertEquals("userCallbackSetSeverity", report.getSeverityReason().getType()); - } - - private void callUnhandledTypeMismatchExceptionEndpoint() { - this.restTemplate.getForEntity( - "/throw-type-mismatch-exception", String.class); - } - - private void callHandledTypeMismatchExceptionUserSeverityEndpoint() { - this.restTemplate.getForEntity( - "/handled-type-mismatch-exception-user-severity", String.class); - } - - private void callHandledTypeMismatchExceptionCallbackSeverityEndpoint() { - this.restTemplate.getForEntity( - "/handled-type-mismatch-exception-callback-severity", String.class); - } - - private void callRuntimeExceptionEndpoint() { - HttpHeaders headers = new HttpHeaders(); - headers.add("header1", "header1Val1"); - headers.add("header1", "header1Val2"); - headers.add("header2", "header2Val1"); - HttpEntity entity = new HttpEntity("parameters", headers); - this.restTemplate.exchange( - "/throw-runtime-exception?param1=paramVal1¶m2=paramVal2", - HttpMethod.GET, - entity, - String.class); - } - - private void verifyNoReport() { - verify(delivery, times(0)).deliver( - any(Serializer.class), - any(), - anyMapOf(String.class, String.class)); - } - - private void assertSessionsStarted(int sessionsStarted) { - assertEquals(sessionsStartedBeforeTest + sessionsStarted, getSessionCount()); - } - - private long getSessionCount() { - return bugsnag.getSessionTracker().getBatchCount() != null - ? bugsnag.getSessionTracker().getBatchCount().getSessionsStarted() : 0; - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringScheduledTaskTest.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringScheduledTaskTest.java deleted file mode 100644 index 7a86ff61..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/SpringScheduledTaskTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.bugsnag; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.bugsnag.HandledState.SeverityReasonType; -import com.bugsnag.delivery.Delivery; -import com.bugsnag.testapp.springboot.TestSpringBootApplication; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.util.ErrorHandler; - -import java.util.Collections; -import java.util.concurrent.ExecutionException; - -/** - * Test that a Spring Boot application configured with the - * {@link BugsnagSpringConfiguration} performs as expected. - */ -@RunWith(SpringRunner.class) -@SpringBootTest(classes = TestSpringBootApplication.class) -public class SpringScheduledTaskTest { - - @Autowired - private Bugsnag bugsnag; - - @Autowired - private ThreadPoolTaskScheduler scheduler; - - @MockBean - private ErrorHandler mockErrorHandler; - - private Delivery delivery; - - /** - * Initialize test state - */ - @Before - public void setUp() { - delivery = mock(Delivery.class); - bugsnag.setDelivery(delivery); - } - - @Test - public void bugsnagNotifyWhenScheduledTaskException() - throws ExecutionException, InterruptedException { - - // The task to schedule - Runnable exampleRunnable = new Runnable() { - @Override - public void run() { - throw new RuntimeException("Scheduled test"); - } - }; - - // Run the task now and wait for it to finish - scheduler.submit(exampleRunnable).get(); - - Report report = TestUtils.verifyAndGetReport(delivery); - - // Assert that the exception was detected correctly - assertEquals("Scheduled test", report.getExceptionMessage()); - assertEquals("java.lang.RuntimeException", report.getExceptionName()); - - // Assert that the severity, severity reason and unhandled values are correct - Assert.assertEquals(Severity.ERROR.getValue(), report.getSeverity()); - assertEquals( - SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), - report.getSeverityReason().getType()); - assertThat( - report.getSeverityReason().getAttributes(), - is(Collections.singletonMap("framework", "Spring"))); - assertTrue(report.getUnhandled()); - - // Assert that the exception is passed to an existing exception handler - ArgumentCaptor exceptionCaptor = - ArgumentCaptor.forClass(RuntimeException.class); - verify(mockErrorHandler, times(1)).handleError(exceptionCaptor.capture()); - assertEquals("Scheduled test", exceptionCaptor.getValue().getMessage()); - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/AsyncService.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/AsyncService.java deleted file mode 100644 index 54376bac..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/AsyncService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.bugsnag.testapp.springboot; - -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.util.concurrent.Future; - -@Service -public class AsyncService { - @Async - public void throwExceptionVoid() { - throw new RuntimeException("Async void test"); - } - - @Async - public Future throwExceptionFuture() { - throw new RuntimeException("Async future test"); - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestConfiguration.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestConfiguration.java deleted file mode 100644 index c1691243..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestConfiguration.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.bugsnag.testapp.springboot; - -import com.bugsnag.Bugsnag; -import com.bugsnag.BugsnagAsyncExceptionHandler; -import com.bugsnag.BugsnagSpringConfiguration; - -import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.scheduling.annotation.AsyncConfigurerSupport; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.util.ErrorHandler; - -/** - * This test configuration loads the BugsnagSpringConfiguration - * that will be used for real Spring bugsnag integration. - */ -@Configuration -@Import(BugsnagSpringConfiguration.class) -public class TestConfiguration extends AsyncConfigurerSupport implements SchedulingConfigurer { - - @Autowired(required = false) - private ErrorHandler scheduledTaskErrorHandler; - - @Bean - public Bugsnag bugsnag() { - return new Bugsnag("apiKey"); - } - - @Bean - ThreadPoolTaskScheduler scheduler() { - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - taskScheduler.setErrorHandler(scheduledTaskErrorHandler); - return taskScheduler; - } - - @Override - public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { - taskRegistrar.setScheduler(scheduler()); - } - - @Override - public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { - return new BugsnagAsyncExceptionHandler(bugsnag()); - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestController.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestController.java deleted file mode 100644 index 9d0091b5..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestController.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.bugsnag.testapp.springboot; - -import com.bugsnag.Bugsnag; -import com.bugsnag.Report; -import com.bugsnag.Severity; -import com.bugsnag.callbacks.Callback; - -import org.springframework.beans.TypeMismatchException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class TestController { - - @Autowired - private Bugsnag bugsnag; - - /** - * Throw a runtime exception - */ - @RequestMapping("/throw-runtime-exception") - public void throwRuntimeException() { - throw new RuntimeException("Test"); - } - - /** - * Throw an exception where the severity reason is exceptionClass - */ - @RequestMapping("/throw-type-mismatch-exception") - public void throwTypeMismatchException() { - throw new TypeMismatchException("Test", String.class); - } - - /** - * Report a handled exception where the severity reason is exceptionClass - */ - @RequestMapping("/handled-type-mismatch-exception") - public void handledTypeMismatchException() { - try { - throw new TypeMismatchException("Test", String.class); - } catch (TypeMismatchException ex) { - bugsnag.notify(ex); - } - } - - /** - * Report a handled exception where the severity is set in the notify call - */ - @RequestMapping("/handled-type-mismatch-exception-user-severity") - public void handledTypeMismatchExceptionUserSeverity() { - try { - throw new TypeMismatchException("Test", String.class); - } catch (TypeMismatchException ex) { - bugsnag.notify(ex, Severity.WARNING); - } - } - - /** - * Report a handled exception where the severity reason is set in a callback - */ - @RequestMapping("/handled-type-mismatch-exception-callback-severity") - public void handledTypeMismatchExceptionCallbackSeverity() { - try { - throw new TypeMismatchException("Test", String.class); - } catch (TypeMismatchException ex) { - bugsnag.notify(ex, new Callback() { - @Override - public void beforeNotify(Report report) { - report.setSeverity(Severity.WARNING); - } - }); - } - } -} diff --git a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java b/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java deleted file mode 100644 index f02cde84..00000000 --- a/bugsnag-spring/src/javaxTest/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.bugsnag.testapp.springboot; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; - -@SpringBootApplication -@EnableScheduling -@EnableAsync -public class TestSpringBootApplication { - public static void main(String[] args) { - SpringApplication.run(TestSpringBootApplication.class, args); - } -} diff --git a/bugsnag-spring/src/common/java/com/bugsnag/BugsnagAsyncExceptionHandler.java b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagAsyncExceptionHandler.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/BugsnagAsyncExceptionHandler.java rename to bugsnag-spring/src/main/java/com/bugsnag/BugsnagAsyncExceptionHandler.java diff --git a/bugsnag-spring/src/jakarta/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java similarity index 100% rename from bugsnag-spring/src/jakarta/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java rename to bugsnag-spring/src/main/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java diff --git a/bugsnag-spring/src/common/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java rename to bugsnag-spring/src/main/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java diff --git a/bugsnag-spring/src/common/java/com/bugsnag/BugsnagSpringConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java similarity index 96% rename from bugsnag-spring/src/common/java/com/bugsnag/BugsnagSpringConfiguration.java rename to bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java index 86d503ef..36f70550 100644 --- a/bugsnag-spring/src/common/java/com/bugsnag/BugsnagSpringConfiguration.java +++ b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java @@ -15,7 +15,7 @@ * Configuration to integrate Bugsnag with Spring. */ @Configuration -@Import(BugsnagImportSelector.class) +@Import({SpringBootJakartaConfiguration.class, JakartaMvcConfiguration.class, ScheduledTaskConfiguration.class}) public class BugsnagSpringConfiguration implements InitializingBean { @Autowired diff --git a/bugsnag-spring/src/common/java/com/bugsnag/ExceptionClassCallback.java b/bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/ExceptionClassCallback.java rename to bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java diff --git a/bugsnag-spring/src/jakarta/java/com/bugsnag/JakartaMvcConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/JakartaMvcConfiguration.java similarity index 100% rename from bugsnag-spring/src/jakarta/java/com/bugsnag/JakartaMvcConfiguration.java rename to bugsnag-spring/src/main/java/com/bugsnag/JakartaMvcConfiguration.java diff --git a/bugsnag-spring/src/common/java/com/bugsnag/ScheduledTaskBeanLocator.java b/bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskBeanLocator.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/ScheduledTaskBeanLocator.java rename to bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskBeanLocator.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java new file mode 100644 index 00000000..9188d7f9 --- /dev/null +++ b/bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java @@ -0,0 +1,197 @@ +package com.bugsnag; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.util.ErrorHandler; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Add configuration for reporting unhandled exceptions for scheduled tasks. + */ +@Configuration +class ScheduledTaskConfiguration implements SchedulingConfigurer { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTaskConfiguration.class); + + @Autowired + private Bugsnag bugsnag; + + @Autowired + private ScheduledTaskBeanLocator beanLocator; + + /** + * Optional: if the app defines a dedicated ErrorHandler bean for scheduled tasks + * (e.g. your @MockBean(name = "scheduledTaskErrorHandler") in tests), we can + * still chain it when we replace or wrap the scheduler. + */ + @Autowired(required = false) + @Qualifier("scheduledTaskErrorHandler") + private ErrorHandler scheduledTaskErrorHandlerBean; + + /** + * Add Bugsnag error handling to the task scheduler being used by Spring. + */ + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + BugsnagScheduledTaskExceptionHandler bugsnagErrorHandler = + new BugsnagScheduledTaskExceptionHandler(bugsnag); + + // Decision process for finding a TaskScheduler, in order of preference: + // 1. use the scheduler from the task registrar + // 2. search for a TaskScheduler bean, by type, then by name + // 3. search for a ScheduledExecutorService bean by type, then by name, and wrap it + // 4. create our own TaskScheduler + TaskScheduler registrarScheduler = taskRegistrar.getScheduler(); + TaskScheduler taskScheduler = registrarScheduler != null + ? registrarScheduler + : beanLocator.resolveTaskScheduler(); + + if (taskScheduler != null) { + // Spring Boot 3 creates a TaskSchedulerRouter which cannot be configured. + // In this case, create our own scheduler instead (but preserve any bean-level handler). + String schedulerClassName = taskScheduler.getClass().getName(); + if (schedulerClassName.equals("org.springframework.scheduling.config.TaskSchedulerRouter")) { + ScheduledExecutorService executorService = beanLocator.resolveScheduledExecutorService(); + chainExistingBeanHandlerIfPresent(bugsnagErrorHandler); + taskScheduler = createNewTaskScheduler(executorService, bugsnagErrorHandler); + taskRegistrar.setScheduler(taskScheduler); + return; + } + + // If it's a proxy, unwrap to the target to allow reflection / method calls. + if (AopUtils.isAopProxy(taskScheduler)) { + Class targetClass = AopProxyUtils.ultimateTargetClass(taskScheduler); + if (TaskScheduler.class.isAssignableFrom(targetClass)) { + TaskScheduler target = (TaskScheduler) AopProxyUtils.getSingletonTarget(taskScheduler); + if (target != null) { + taskScheduler = target; + } + } + } + + configureExistingTaskScheduler(taskScheduler, bugsnagErrorHandler); + } else { + // No scheduler has been defined by the application, create one and add the Bugsnag error handler. + ScheduledExecutorService executorService = beanLocator.resolveScheduledExecutorService(); + chainExistingBeanHandlerIfPresent(bugsnagErrorHandler); + taskScheduler = createNewTaskScheduler(executorService, bugsnagErrorHandler); + taskRegistrar.setScheduler(taskScheduler); + } + } + + private TaskScheduler createNewTaskScheduler( + ScheduledExecutorService executorService, + BugsnagScheduledTaskExceptionHandler errorHandler + ) { + if (executorService != null) { + // Create a task scheduler which delegates to the existing Executor + ConcurrentTaskScheduler scheduler = new ConcurrentTaskScheduler(executorService); + scheduler.setErrorHandler(errorHandler); + return scheduler; + } else { + // If no task scheduler has been defined by the application, create one and add the Bugsnag error handler. + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setErrorHandler(errorHandler); + scheduler.initialize(); + return scheduler; + } + } + + /** + * If a task scheduler has been defined by the application, configure it so that Bugsnag error handling is added. + * We first capture any existing ErrorHandler (e.g. your mock bean), chain it into the Bugsnag handler, + * then set the Bugsnag handler via setter if available (Boot 3+) or via field reflection. + */ + private void configureExistingTaskScheduler( + TaskScheduler taskScheduler, + BugsnagScheduledTaskExceptionHandler errorHandler + ) { + // (1) Capture whatever handler is already configured on the scheduler + ErrorHandler existing = extractExistingErrorHandler(taskScheduler); + if (existing != null) { + errorHandler.setExistingErrorHandler(existing); + } else if (scheduledTaskErrorHandlerBean != null) { + // Fallback: chain the bean-level handler if the scheduler didn't have one yet + errorHandler.setExistingErrorHandler(scheduledTaskErrorHandlerBean); + } + + // (2) Install the Bugsnag handler via public setter if available, else via private field + if (trySetErrorHandlerViaMethod(taskScheduler, errorHandler)) { + return; + } + trySetErrorHandlerViaField(taskScheduler, errorHandler); + } + + /** + * Chain the dedicated bean-level handler if present (useful when we replace the scheduler). + */ + private void chainExistingBeanHandlerIfPresent(BugsnagScheduledTaskExceptionHandler errorHandler) { + if (scheduledTaskErrorHandlerBean != null) { + errorHandler.setExistingErrorHandler(scheduledTaskErrorHandlerBean); + } + } + + /** + * Prefer a public getter if present; otherwise fall back to private field. + */ + private ErrorHandler extractExistingErrorHandler(TaskScheduler taskScheduler) { + // Try public getter (present on ThreadPoolTaskScheduler et al.) + try { + Method getter = taskScheduler.getClass().getMethod("getErrorHandler"); + Object val = getter.invoke(taskScheduler); + if (val instanceof ErrorHandler) { + return (ErrorHandler) val; + } + } catch (Throwable ignore) { + // no-op + } + + // Fall back to private field access + try { + Field fld = taskScheduler.getClass().getDeclaredField("errorHandler"); + fld.setAccessible(true); + Object val = fld.get(taskScheduler); + if (val instanceof ErrorHandler) { + return (ErrorHandler) val; + } + } catch (Throwable ignore) { + // no-op + } + + return null; + } + + private boolean trySetErrorHandlerViaMethod(TaskScheduler taskScheduler, ErrorHandler handler) { + try { + Method setter = taskScheduler.getClass().getMethod("setErrorHandler", ErrorHandler.class); + setter.invoke(taskScheduler, handler); + return true; + } catch (Throwable ex) { + return false; + } + } + + private boolean trySetErrorHandlerViaField(TaskScheduler taskScheduler, ErrorHandler handler) { + try { + Field fld = taskScheduler.getClass().getDeclaredField("errorHandler"); + fld.setAccessible(true); + fld.set(taskScheduler, handler); + return true; + } catch (Throwable ex) { + return false; + } + } +} diff --git a/bugsnag-spring/src/common/java/com/bugsnag/SpringBootConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/SpringBootConfiguration.java rename to bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java diff --git a/bugsnag-spring/src/jakarta/java/com/bugsnag/SpringBootJakartaConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootJakartaConfiguration.java similarity index 78% rename from bugsnag-spring/src/jakarta/java/com/bugsnag/SpringBootJakartaConfiguration.java rename to bugsnag-spring/src/main/java/com/bugsnag/SpringBootJakartaConfiguration.java index c9f0d413..06e02edf 100644 --- a/bugsnag-spring/src/jakarta/java/com/bugsnag/SpringBootJakartaConfiguration.java +++ b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootJakartaConfiguration.java @@ -16,9 +16,8 @@ class SpringBootJakartaConfiguration extends SpringBootConfiguration { /** - * The {@link com.bugsnag.servlet.javax.BugsnagServletContainerInitializer} does not work for Spring Boot, need to - * register the {@link BugsnagServletRequestListener} using a Spring Boot - * {@link ServletListenerRegistrationBean} instead. This adds session tracking and + * Spring Boot requires manual registration of the {@link BugsnagServletRequestListener} using a Spring Boot + * {@link ServletListenerRegistrationBean}. This adds session tracking and * automatic servlet request metadata collection. */ @Bean diff --git a/bugsnag-spring/src/common/java/com/bugsnag/SpringBootLoadedCondition.java b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootLoadedCondition.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/SpringBootLoadedCondition.java rename to bugsnag-spring/src/main/java/com/bugsnag/SpringBootLoadedCondition.java diff --git a/bugsnag-spring/src/common/java/com/bugsnag/SpringWebMvcLoadedCondition.java b/bugsnag-spring/src/main/java/com/bugsnag/SpringWebMvcLoadedCondition.java similarity index 100% rename from bugsnag-spring/src/common/java/com/bugsnag/SpringWebMvcLoadedCondition.java rename to bugsnag-spring/src/main/java/com/bugsnag/SpringWebMvcLoadedCondition.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/NotifierTest.java b/bugsnag-spring/src/test/java/com/bugsnag/NotifierTest.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/NotifierTest.java rename to bugsnag-spring/src/test/java/com/bugsnag/NotifierTest.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java b/bugsnag-spring/src/test/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java rename to bugsnag-spring/src/test/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/ScheduledTaskConfigurationTest.java b/bugsnag-spring/src/test/java/com/bugsnag/ScheduledTaskConfigurationTest.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/ScheduledTaskConfigurationTest.java rename to bugsnag-spring/src/test/java/com/bugsnag/ScheduledTaskConfigurationTest.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/SpringAsyncTest.java b/bugsnag-spring/src/test/java/com/bugsnag/SpringAsyncTest.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/SpringAsyncTest.java rename to bugsnag-spring/src/test/java/com/bugsnag/SpringAsyncTest.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/SpringMvcTest.java b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/SpringMvcTest.java rename to bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/SpringScheduledTaskTest.java b/bugsnag-spring/src/test/java/com/bugsnag/SpringScheduledTaskTest.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/SpringScheduledTaskTest.java rename to bugsnag-spring/src/test/java/com/bugsnag/SpringScheduledTaskTest.java diff --git a/bugsnag-spring/src/commonTest/java/com/bugsnag/TestUtils.java b/bugsnag-spring/src/test/java/com/bugsnag/TestUtils.java similarity index 100% rename from bugsnag-spring/src/commonTest/java/com/bugsnag/TestUtils.java rename to bugsnag-spring/src/test/java/com/bugsnag/TestUtils.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/AsyncService.java b/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/AsyncService.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/AsyncService.java rename to bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/AsyncService.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/TestConfiguration.java b/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestConfiguration.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/TestConfiguration.java rename to bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestConfiguration.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/TestController.java b/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestController.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/TestController.java rename to bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestController.java diff --git a/bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java b/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java similarity index 100% rename from bugsnag-spring/src/jakartaTest/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java rename to bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java diff --git a/bugsnag/build.gradle b/bugsnag/build.gradle index 140e8a75..2d11d351 100644 --- a/bugsnag/build.gradle +++ b/bugsnag/build.gradle @@ -6,13 +6,13 @@ apply plugin: 'java-library' apply from: '../common.gradle' compileJava { - sourceCompatibility = '1.7' - targetCompatibility = '1.7' + sourceCompatibility = '17' + targetCompatibility = '17' } compileTestJava { - sourceCompatibility = '1.7' - targetCompatibility = '1.7' + sourceCompatibility = '17' + targetCompatibility = '17' } repositories { @@ -22,7 +22,6 @@ repositories { dependencies { api "com.fasterxml.jackson.core:jackson-databind:2.14.1" api "org.slf4j:slf4j-api:${slf4jApiVersion}" - compileOnly "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" compileOnly "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" compileOnly("ch.qos.logback:logback-classic:${logbackVersion}") { exclude group: "org.slf4j" @@ -30,7 +29,6 @@ dependencies { testImplementation "junit:junit:${junitVersion}" testImplementation "org.slf4j:log4j-over-slf4j:${slf4jApiVersion}" - testImplementation "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" testImplementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" testImplementation "org.mockito:mockito-core:${mockitoVersion}" testImplementation("ch.qos.logback:logback-classic:${logbackVersion}") { @@ -50,4 +48,12 @@ downloadLicenses { java { withJavadocJar() -} \ No newline at end of file +} + +/** ---- Publishing config ---- + * Pulls in publishing+signing rules from the shared release.gradle. + * This will create tasks like: + * :bugsnag:publishMavenJavaPublicationToTestRepository + * :bugsnag:publishMavenJavaPublicationToOssrhStagingRepository + */ +apply from: "${rootProject.projectDir}/release.gradle" \ No newline at end of file diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index 351c1c0d..a55828e4 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -4,7 +4,6 @@ import com.bugsnag.callbacks.Callback; import com.bugsnag.callbacks.DeviceCallback; import com.bugsnag.callbacks.JakartaServletCallback; -import com.bugsnag.callbacks.JavaxServletCallback; import com.bugsnag.delivery.AsyncHttpDelivery; import com.bugsnag.delivery.Delivery; import com.bugsnag.delivery.HttpDelivery; @@ -61,10 +60,6 @@ public class Configuration { this.delivery = new AsyncHttpDelivery(endpointConfiguration.getNotifyEndpoint()); this.sessionDelivery = new AsyncHttpDelivery(endpointConfiguration.getSessionEndpoint()); - if (JavaxServletCallback.isAvailable()) { - addCallback(new JavaxServletCallback()); - } - if (JakartaServletCallback.isAvailable()) { addCallback(new JakartaServletCallback()); } diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java deleted file mode 100644 index 233fc7ba..00000000 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.bugsnag.callbacks; - -import com.bugsnag.Report; -import com.bugsnag.servlet.BugsnagServletRequestListener; - -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import javax.servlet.http.HttpServletRequest; - -public class JavaxServletCallback implements Callback { - private static final String HEADER_X_FORWARDED_FOR = "X-FORWARDED-FOR"; - - /** - * @return true if the servlet request listener is available. - */ - public static boolean isAvailable() { - try { - Class.forName("javax.servlet.ServletRequestListener", false, - JavaxServletCallback.class.getClassLoader()); - return true; - } catch (ClassNotFoundException ex) { - return false; - } - } - - @Override - public void beforeNotify(Report report) { - // Check if we have any servlet request data available - HttpServletRequest request = BugsnagServletRequestListener.getServletRequest(); - if (request == null) { - return; - } - - // Add request information to metaData - report - .addToTab("request", "url", request.getRequestURL().toString()) - .addToTab("request", "method", request.getMethod()) - .addToTab("request", "params", - new HashMap(request.getParameterMap())) - .addToTab("request", "clientIp", getClientIp(request)) - .addToTab("request", "headers", getHeaderMap(request)); - - // Set default context - if (report.getContext() == null) { - report.setContext(request.getMethod() + " " + request.getRequestURI()); - } - } - - private String getClientIp(HttpServletRequest request) { - String remoteAddr = request.getRemoteAddr(); - String forwardedAddr = request.getHeader(HEADER_X_FORWARDED_FOR); - if (forwardedAddr != null) { - remoteAddr = forwardedAddr; - int idx = remoteAddr.indexOf(','); - if (idx > -1) { - remoteAddr = remoteAddr.substring(0, idx); - } - } - return remoteAddr; - } - - private Map getHeaderMap(HttpServletRequest request) { - Map headers = new HashMap(); - Enumeration headerNames = request.getHeaderNames(); - - while (headerNames != null && headerNames.hasMoreElements()) { - String key = headerNames.nextElement(); - Enumeration headerValues = request.getHeaders(key); - StringBuilder value = new StringBuilder(); - - if (headerValues != null && headerValues.hasMoreElements()) { - value.append(headerValues.nextElement()); - - // If there are multiple values for the header, do comma-separated concat - // as per RFC 2616: - // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - while (headerValues.hasMoreElements()) { - value.append(",").append(headerValues.nextElement()); - } - } - - headers.put(key, value.toString()); - } - - return headers; - } -} diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java b/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java deleted file mode 100644 index a6c498cf..00000000 --- a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.bugsnag.servlet; - -/** - * @see com.bugsnag.servlet.javax.BugsnagServletContainerInitializer - * @deprecated since 3.7.1 - to be replaced with {@code com.bugsnag.servlet.javax.BugsnagServletContainerInitializer} - */ -@Deprecated -public class BugsnagServletContainerInitializer extends com.bugsnag.servlet.javax.BugsnagServletContainerInitializer { -} diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java b/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java deleted file mode 100644 index d10155c1..00000000 --- a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.bugsnag.servlet; - -/** - * @see com.bugsnag.servlet.javax.BugsnagServletRequestListener - * @deprecated since 3.7.1 - to be replaced with {@code com.bugsnag.servlet.javax.BugsnagServletRequestListener} - */ -@Deprecated -public class BugsnagServletRequestListener extends com.bugsnag.servlet.javax.BugsnagServletRequestListener { -} diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java b/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java deleted file mode 100644 index af177c35..00000000 --- a/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.bugsnag.servlet.javax; - -import java.util.Set; -import javax.servlet.ServletContainerInitializer; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; - -public class BugsnagServletContainerInitializer implements ServletContainerInitializer { - @Override - public void onStartup(Set> cls, ServletContext context) throws ServletException { - context.addListener(BugsnagServletRequestListener.class); - } -} diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java b/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java deleted file mode 100644 index a3e9e47a..00000000 --- a/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.bugsnag.servlet.javax; - -import com.bugsnag.Bugsnag; - -import javax.servlet.ServletRequest; -import javax.servlet.ServletRequestEvent; -import javax.servlet.ServletRequestListener; -import javax.servlet.http.HttpServletRequest; - -public class BugsnagServletRequestListener implements ServletRequestListener { - - private static final ThreadLocal SERVLET_REQUEST = - new ThreadLocal(); - - public static HttpServletRequest getServletRequest() { - return SERVLET_REQUEST.get(); - } - - @Override - public void requestInitialized(ServletRequestEvent servletRequestEvent) { - trackServletSession(); - ServletRequest servletRequest = servletRequestEvent.getServletRequest(); - - if (servletRequest instanceof HttpServletRequest) { - SERVLET_REQUEST.set((HttpServletRequest) servletRequest); - } - } - - @Override - public void requestDestroyed(ServletRequestEvent servletRequestEvent) { - SERVLET_REQUEST.remove(); - Bugsnag.clearThreadMetaData(); - } - - private void trackServletSession() { - for (Bugsnag bugsnag : Bugsnag.uncaughtExceptionClients()) { - if (bugsnag.shouldAutoCaptureSessions()) { - bugsnag.startSession(); - } - } - } -} diff --git a/bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java deleted file mode 100644 index bd51a02b..00000000 --- a/bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.bugsnag; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.bugsnag.callbacks.JavaxServletCallback; - -import com.bugsnag.servlet.BugsnagServletRequestListener; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import javax.servlet.ServletContext; -import javax.servlet.ServletRequestEvent; -import javax.servlet.http.HttpServletRequest; - -public class JavaxServletCallbackTest { - - private Bugsnag bugsnag; - - /** - * Generate a new request instance which will be read by the servlet - * context and callback - */ - @Before - public void setUp() { - bugsnag = new Bugsnag("apikey", false); - bugsnag.setDelivery(null); - - HttpServletRequest request = mock(HttpServletRequest.class); - - Map params = new HashMap(); - params.put("account", new String[]{"Acme Co"}); - params.put("name", new String[]{"Bill"}); - when(request.getParameterMap()).thenReturn(params); - - when(request.getMethod()).thenReturn("PATCH"); - when(request.getRequestURL()).thenReturn(new StringBuffer("/foo/bar")); - when(request.getRequestURI()).thenReturn("/foo/bar"); - when(request.getRemoteAddr()).thenReturn("12.0.4.57"); - - when(request.getHeaderNames()).thenReturn( - stringsToEnumeration( - "Content-Type", - "Content-Length", - "X-Custom-Header", - "Authorization", - "Cookie")); - when(request.getHeaders("Content-Type")).thenReturn( - stringsToEnumeration("application/json")); - when(request.getHeaders("Content-Length")).thenReturn( - stringsToEnumeration("54")); - when(request.getHeaders("X-Custom-Header")).thenReturn( - stringsToEnumeration("some-data-1", "some-data-2")); - when(request.getHeaders("Authorization")).thenReturn( - stringsToEnumeration("Basic ABC123")); - when(request.getHeaders("Cookie")).thenReturn( - stringsToEnumeration("name1=val1; name2=val2")); - - ServletContext context = mock(ServletContext.class); - BugsnagServletRequestListener listener = new BugsnagServletRequestListener(); - listener.requestInitialized(new ServletRequestEvent(context, request)); - } - - /** - * Close test Bugsnag - */ - @After - public void closeBugsnag() { - bugsnag.close(); - } - - @SuppressWarnings("unchecked") - @Test - public void testRequestMetadataAdded() { - Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); - JavaxServletCallback callback = new JavaxServletCallback(); - callback.beforeNotify(report); - - Map metadata = report.getMetaData(); - assertTrue(metadata.containsKey("request")); - - Map request = (Map) metadata.get("request"); - assertEquals("/foo/bar", request.get("url")); - assertEquals("PATCH", request.get("method")); - assertEquals("12.0.4.57", request.get("clientIp")); - - assertTrue(request.containsKey("headers")); - Map headers = (Map) request.get("headers"); - assertEquals("application/json", headers.get("Content-Type")); - assertEquals("54", headers.get("Content-Length")); - assertEquals("some-data-1,some-data-2", headers.get("X-Custom-Header")); - - // Make sure that actual Authorization header value is not in the report - assertEquals("[FILTERED]", headers.get("Authorization")); - - // Make sure that actual cookies are not in the report - assertEquals("[FILTERED]", headers.get("Cookie")); - - assertTrue(request.containsKey("params")); - Map params = (Map) request.get("params"); - assertTrue(params.containsKey("account")); - String[] account = params.get("account"); - assertEquals("Acme Co", account[0]); - - assertTrue(params.containsKey("name")); - String[] name = params.get("name"); - assertEquals("Bill", name[0]); - } - - @Test - public void testRequestContextSet() { - Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); - JavaxServletCallback callback = new JavaxServletCallback(); - callback.beforeNotify(report); - - assertEquals("PATCH /foo/bar", report.getContext()); - } - - @Test - public void testExistingContextNotOverridden() { - Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); - report.setContext("Honey nut corn flakes"); - JavaxServletCallback callback = new JavaxServletCallback(); - callback.beforeNotify(report); - - assertEquals("Honey nut corn flakes", report.getContext()); - } - - private Report generateReport(java.lang.Exception exception) { - return bugsnag.buildReport(exception); - } - - private Enumeration stringsToEnumeration(String... strings) { - return Collections.enumeration(Arrays.asList(strings)); - } -} diff --git a/build.gradle b/build.gradle index 37e5c3ff..b0895907 100644 --- a/build.gradle +++ b/build.gradle @@ -12,4 +12,10 @@ gradle.projectsEvaluated { // fail build on warnings, disable options complaining about Java 6 compatibility when building with JDK 7+ options.compilerArgs << "-Xlint:all" << "-Werror" << "-Xlint:-options" } +} + +tasks.register('publishToTestRepoAll') { + dependsOn subprojects.collect { + tasks.findByPath("${it.path}:publishAllPublicationsToTestRepository") ?: [] + }.flatten() } \ No newline at end of file diff --git a/common.gradle b/common.gradle index d69f4398..4c803b16 100644 --- a/common.gradle +++ b/common.gradle @@ -1,5 +1,4 @@ ext { - javaxServletApiVersion = "3.1.0" jakartaServletApiVersion = "5.0.0" logbackVersion = "1.2.3" slf4jApiVersion = "1.7.25" diff --git a/examples/servlet-javax/README.md b/examples/servlet-javax/README.md deleted file mode 100644 index b81c6e81..00000000 --- a/examples/servlet-javax/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Bugsnag Javax Servlet Example - -Demonstrates how to use Bugsnag in a Servlet-based Java application. - -1. Open `ExampleServlet` and alter the value of `bugsnag = new Bugsnag("YOUR-API-KEY");` to match your API key - -2. Build the app - - ```shell - gradle clean assemble - ``` - -3. Start the web server - - ```shell - gradle appRun - ``` - -4. Cause a crash by visiting [http://localhost:8080/servlet](http://localhost:8080/servlet) - -5. View the captured errors in [your dashboard](https://app.bugsnag.com) diff --git a/examples/servlet-javax/build.gradle b/examples/servlet-javax/build.gradle deleted file mode 100644 index 1e4412f3..00000000 --- a/examples/servlet-javax/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'java' -apply plugin: 'war' -apply plugin: 'org.gretty' - -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath 'org.gretty:gretty:3.1.1' - } -} - -repositories { - mavenCentral() -} - -dependencies { - runtimeOnly 'org.slf4j:slf4j-simple:1.7.25' - implementation "javax.servlet:javax.servlet-api:3.1.0" - implementation project(':bugsnag') -} - -gretty { - contextPath = '/' - jvmArgs = ['-Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG'] -} - diff --git a/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java b/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java deleted file mode 100644 index 317a517f..00000000 --- a/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.bugsnag.example.servlet; - -import com.bugsnag.Bugsnag; - -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ErrorHandler extends HttpServlet { - - private static final long serialVersionUID = 4926619146717832212L; - - private Bugsnag bugsnag; - - /** - * Error handler to report the error to Bugsnag - */ - public ErrorHandler() { - bugsnag = new Bugsnag("YOUR-API-KEY"); - bugsnag.setProjectPackages("com.bugsnag.example"); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - // Notify Bugsnag of the exception - Throwable throwable = (Throwable) req.getAttribute("javax.servlet.error.exception"); - bugsnag.notify(throwable); - } -} diff --git a/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java b/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java deleted file mode 100644 index a33b7fdc..00000000 --- a/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.bugsnag.example.servlet; - -import com.bugsnag.Bugsnag; -import com.bugsnag.Severity; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ExampleServlet extends HttpServlet { - - private static final long serialVersionUID = 1432171052111530587L; - - private Bugsnag bugsnag; - - /** - * Simple servlet example - */ - public ExampleServlet() { - bugsnag = new Bugsnag("YOUR-API-KEY"); - bugsnag.setProjectPackages("com.bugsnag.example"); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { - // Send a handled exception to Bugsnag - try { - throw new RuntimeException("Handled exception - default severity"); - } catch (RuntimeException e) { - bugsnag.notify(e); - } - - // Send a handled exception to Bugsnag with info severity - try { - throw new RuntimeException("Handled exception - INFO severity"); - } catch (RuntimeException ex) { - bugsnag.notify(ex, Severity.INFO); - } - - // Throw an exception - not automatically reported so must be handled by the error handler - throw new ServletException("Servlet exception"); - } -} diff --git a/examples/servlet-javax/src/main/webapp/WEB-INF/web.xml b/examples/servlet-javax/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index e34c77b7..00000000 --- a/examples/servlet-javax/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - ExampleServlet - ExampleServlet - com.bugsnag.example.servlet.ExampleServlet - - - ErrorHandler - com.bugsnag.example.servlet.ErrorHandler - - - - ExampleServlet - / - - - ErrorHandler - /ErrorHandler - - - - - javax.servlet.ServletException - /ErrorHandler - - diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle index 2425932b..bd500ac6 100644 --- a/features/fixtures/mazerunner/build.gradle +++ b/features/fixtures/mazerunner/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '2.1.1.RELEASE' + springBootVersion = '3.5.6' } repositories { mavenCentral() @@ -17,7 +17,7 @@ apply plugin: 'io.spring.dependency-management' group 'com.bugsnag.mazerunner' version '1.0-SNAPSHOT' -sourceCompatibility = '1.8' +sourceCompatibility = '17' repositories { mavenCentral() @@ -28,13 +28,14 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") - implementation("ch.qos.logback:logback-classic:1.2.3") + implementation("ch.qos.logback:logback-classic:1.5.18") + implementation("ch.qos.logback:logback-core:1.5.18") + implementation("org.codehaus.janino:janino:3.1.10") - implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.1") - implementation("com.fasterxml.jackson.core:jackson-databind:2.9.1") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.1") implementation("com.bugsnag:bugsnag:9.9.9-test") implementation project(":scenarios") testImplementation group: 'junit', name: 'junit', version: '4.13.2' } - diff --git a/features/fixtures/mazerunnerplainspring/build.gradle b/features/fixtures/mazerunnerplainspring/build.gradle deleted file mode 100644 index beb828fd..00000000 --- a/features/fixtures/mazerunnerplainspring/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -plugins { - id "war" -} - -group 'com.bugsnag.mazerunnerplainspring' - -sourceCompatibility = '1.8' - -repositories { - mavenCentral() - maven { - url file('../libs').toURI() - } -} - -dependencies { - implementation("org.springframework:spring-webmvc:4.2.0.RELEASE") - implementation("javax.servlet:javax.servlet-api:3.1.0") - implementation("ch.qos.logback:logback-classic:1.2.3") - - implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.1") - implementation("com.fasterxml.jackson.core:jackson-databind:2.9.1") - implementation("com.bugsnag:bugsnag:9.9.9-test") - implementation("com.bugsnag:bugsnag-spring:9.9.9-test") - implementation project(":scenarios") -} - -war { - archiveName = 'mazerunnerplainspring.war' -} \ No newline at end of file diff --git a/features/fixtures/mazerunnerplainspring/gradle/wrapper/gradle-wrapper.jar b/features/fixtures/mazerunnerplainspring/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 6b6ea3ab4ff4f69d55c5fd9c0a6ac70f47d41008..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54731 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�A``0fKpc0WAU$bg;D1GyIQa;7hdW#MKpuc+fPkN63DB8-QCon}w55{+ zK*~kT%+g5EzyQD?t7onEf4S{n5`Y%HU(o_IxB{TT|7ske_`f6&x7P#Y(^Lk?K1$eF z8Ohk#SUB1Kx@itd&0`AyGX5-pngUK={i!e#{R8yBlYsrOyWkeC%j+RF5iuzNd@qNfB>rpz#I8f@s{`{nv9Lfe|_68v(EoSLz4a_*l*>a ze`!-MGn)To!IAynEWd7smziIGVoE6e67%Kc<1cw&U)K0#-o>Bj6zac3|F@C;A`9b7 z=$C2lenNw4{S)+GliV}2G}0N|fG z)sG;wmk+am&9E z{i*){Mdkk`{7ZrQpYTT3{{;VHviuVEr7rtV*j}4|g8j3f;U(rv3E-cYWBU$VtD_%M1M5Tztu!v61)^^{7FFK`5OfPAl>+q z>7}IGPbO2}-(dPJfwz~OFNL~(a)Jl^2IoJ?cDeQ`pGlj`8S@I)6PGc(f|02zx2HPq$%$HzbV~+ v^TYh7&j0bc{Ml*p!|U?1+ylV=n-AuVG#FqV^dowa3FsZ*6oN|6kH7vOrq \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/features/fixtures/mazerunnerplainspring/gradlew.bat b/features/fixtures/mazerunnerplainspring/gradlew.bat deleted file mode 100644 index e95643d6..00000000 --- a/features/fixtures/mazerunnerplainspring/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java deleted file mode 100644 index d6389828..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -@Configuration -@EnableWebMvc -@EnableScheduling -@EnableAsync -@ComponentScan(basePackages = "com.bugsnag.mazerunnerplainspring") -public class AppConfig { - @Bean - public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } -} \ No newline at end of file diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java deleted file mode 100644 index 2fd1cf0f..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; - -public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected Class[] getRootConfigClasses() { - return new Class[] { AppConfig.class }; - } - - @Override - protected Class[] getServletConfigClasses() { - return null; - } - - @Override - protected String[] getServletMappings() { - return new String[] { "/" }; - } - -} \ No newline at end of file diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java deleted file mode 100644 index f1d52269..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import com.bugsnag.Bugsnag; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import java.util.concurrent.Future; - -@Service -public class AsyncMethodService { - - @Async - public void doSomethingAsync() { - - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); - - try { - Thread.sleep(100); - } catch (InterruptedException e) { - // ignore - } - - throw new RuntimeException("Unhandled exception from Async method"); - } -} diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java deleted file mode 100644 index d1b0e59e..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import com.bugsnag.Bugsnag; -import com.bugsnag.BugsnagAsyncExceptionHandler; -import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.AsyncConfigurerSupport; - -@Configuration -public class BugsnagAsyncConfig extends AsyncConfigurerSupport { - @Autowired - private Bugsnag bugsnag; - - @Override - public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { - return new BugsnagAsyncExceptionHandler(bugsnag); - } -} diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java deleted file mode 100644 index cf5d52ca..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import com.bugsnag.Bugsnag; -import com.bugsnag.BugsnagSpringConfiguration; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.util.StringUtils; - -@Configuration -@Import(BugsnagSpringConfiguration.class) -public class BugsnagConfig { - - @Value("${BUGSNAG_API_KEY}") - private String bugsnagApiKey; - - @Value("${MAZERUNNER_BASE_URL}") - private String bugsnagEndpoint; - - @Value("${AUTO_CAPTURE_SESSIONS:false}") - private boolean autoCaptureSessions; - - @Bean - public Bugsnag bugsnag() { - Bugsnag bugsnag = new Bugsnag(bugsnagApiKey); - bugsnag.setEndpoints(bugsnagEndpoint + "notify", bugsnagEndpoint + "sessions"); - bugsnag.setAutoCaptureSessions(autoCaptureSessions); - bugsnag.setReleaseStage("production"); - bugsnag.setAppVersion("1.0.0"); - return bugsnag; - } -} diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java deleted file mode 100644 index 3574ff08..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import com.bugsnag.Bugsnag; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -@Service -public class ScheduledTaskService { - - @Value("${RUN_SCHEDULED_TASK:false}") - private boolean throwException; - - private volatile boolean exceptionSent = false; - - @Scheduled(fixedDelay = 3000) - public void doSomething() { - if (throwException && !exceptionSent) { - - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); - - exceptionSent = true; - throw new RuntimeException("Unhandled exception from ScheduledTaskService"); - } - } -} diff --git a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java b/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java deleted file mode 100644 index 59496a2f..00000000 --- a/features/fixtures/mazerunnerplainspring/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.bugsnag.mazerunnerplainspring; - -import com.bugsnag.Bugsnag; -import com.bugsnag.mazerunner.scenarios.Scenario; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; - -@RestController -public class TestRestController { - - private static final Logger LOGGER = LoggerFactory.getLogger(TestRestController.class); - - @Autowired - private Bugsnag bugsnag; - - @Autowired - private AsyncMethodService asyncMethodService; - - @RequestMapping("/") - public String ping() { - return "Plain Spring Fixture app ready for connections"; - } - - @RequestMapping("/send-unhandled-exception") - public void sendUnhandledException() { - throw new RuntimeException("Unhandled exception from TestRestController"); - } - - @RequestMapping("/add-session") - public void addSession() { - // A session should be automatically recorded by Bugsnag if automatic sessions are enabled - LOGGER.info("Starting a new session"); - - // Flush sessions now, otherwise need to wait for sessions to be automatically flushed - flushAllSessions(); - LOGGER.info("Flushed all sessions"); - } - - @RequestMapping("/run-async-task") - public void runAsyncTask() { - try { - asyncMethodService.doSomethingAsync(); - } catch (Exception ex) { - // This should not happen - LOGGER.info("Saw exception from async call"); - } - } - - @RequestMapping("/run-scenario/{scenario}") - public void runScenario(@PathVariable String scenario) { - try { - Class clz = Class.forName("com.bugsnag.mazerunner.scenarios." + scenario); - Constructor constructor = clz.getConstructors()[0]; - ((Scenario) constructor.newInstance(bugsnag)).run(); - } catch (Exception ex) { - LOGGER.error("Error getting scenario", ex); - } - } - - /** - * Flushes sessions from the Bugsnag object - */ - private void flushAllSessions() { - try { - Field field = bugsnag.getClass().getDeclaredField("sessionTracker"); - field.setAccessible(true); - Object sessionTracker = field.get(bugsnag); - - field = sessionTracker.getClass().getDeclaredField("enqueuedSessionCounts"); - field.setAccessible(true); - Collection sessionCounts = (Collection) field.get(sessionTracker); - - // Flush the sessions - Method method = sessionTracker.getClass().getDeclaredMethod("flushSessions", Date.class); - method.setAccessible(true); - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.MINUTE, 2); - method.invoke(sessionTracker, calendar.getTime()); - - // Wait until sessions are flushed - while (sessionCounts.size() > 0) { - Thread.sleep(1000); - } - } catch (Exception ex) { - LOGGER.error("failed to flush sessions", ex); - } - } -} diff --git a/features/fixtures/mazerunnerplainspring6/build.gradle b/features/fixtures/mazerunnerplainspring6/build.gradle index d53ec39a..ab40ef70 100644 --- a/features/fixtures/mazerunnerplainspring6/build.gradle +++ b/features/fixtures/mazerunnerplainspring6/build.gradle @@ -4,6 +4,8 @@ plugins { group 'com.bugsnag.mazerunnerplainspring' +sourceCompatibility = '17' + repositories { mavenCentral() maven { @@ -14,15 +16,16 @@ repositories { dependencies { implementation("org.springframework:spring-webmvc:6.0.0") implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") - implementation("ch.qos.logback:logback-classic:1.2.3") + implementation("ch.qos.logback:logback-classic:1.5.18") + implementation("org.codehaus.janino:janino:3.1.10") - implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.1") - implementation("com.fasterxml.jackson.core:jackson-databind:2.9.1") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.1") implementation("com.bugsnag:bugsnag:9.9.9-test") implementation("com.bugsnag:bugsnag-spring:9.9.9-test") implementation project(":scenarios") } war { - archiveName = 'mazerunnerplainspring.war' + archiveFileName.set('mazerunnerplainspring.war') } \ No newline at end of file diff --git a/features/fixtures/mazerunnerspringboot/build.gradle b/features/fixtures/mazerunnerspringboot/build.gradle deleted file mode 100644 index 65556010..00000000 --- a/features/fixtures/mazerunnerspringboot/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -buildscript { - ext { - springBootVersion = '2.1.1.RELEASE' - } - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") - } -} - -apply plugin: 'java' -apply plugin: 'org.springframework.boot' -apply plugin: 'io.spring.dependency-management' - -group 'com.bugsnag.mazerunnerspringboot' -version '1.0-SNAPSHOT' - -sourceCompatibility = '1.8' - -repositories { - mavenCentral() - maven { - url file('../libs').toURI() - } -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'ch.qos.logback:logback-classic:1.2.3' - - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.1' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.1' - implementation 'com.bugsnag:bugsnag:9.9.9-test' - implementation 'com.bugsnag:bugsnag-spring:9.9.9-test' - implementation project(":scenarios") - - // required for JDK 9 and above - implementation('javax.xml.bind:jaxb-api:2.3.0') - - testImplementation group: 'junit', name: 'junit', version: '4.13.2' -} \ No newline at end of file diff --git a/features/fixtures/mazerunnerspringboot/gradle/wrapper/gradle-wrapper.jar b/features/fixtures/mazerunnerspringboot/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 6b6ea3ab4ff4f69d55c5fd9c0a6ac70f47d41008..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54731 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�A``0fKpc0WAU$bg;D1GyIQa;7hdW#MKpuc+fPkN63DB8-QCon}w55{+ zK*~kT%+g5EzyQD?t7onEf4S{n5`Y%HU(o_IxB{TT|7ske_`f6&x7P#Y(^Lk?K1$eF z8Ohk#SUB1Kx@itd&0`AyGX5-pngUK={i!e#{R8yBlYsrOyWkeC%j+RF5iuzNd@qNfB>rpz#I8f@s{`{nv9Lfe|_68v(EoSLz4a_*l*>a ze`!-MGn)To!IAynEWd7smziIGVoE6e67%Kc<1cw&U)K0#-o>Bj6zac3|F@C;A`9b7 z=$C2lenNw4{S)+GliV}2G}0N|fG z)sG;wmk+am&9E z{i*){Mdkk`{7ZrQpYTT3{{;VHviuVEr7rtV*j}4|g8j3f;U(rv3E-cYWBU$VtD_%M1M5Tztu!v61)^^{7FFK`5OfPAl>+q z>7}IGPbO2}-(dPJfwz~OFNL~(a)Jl^2IoJ?cDeQ`pGlj`8S@I)6PGc(f|02zx2HPq$%$HzbV~+ v^TYh7&j0bc{Ml*p!|U?1+ylV=n-AuVG#FqV^dowa3FsZ*6oN|6kH7vOrq \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/features/fixtures/mazerunnerspringboot/gradlew.bat b/features/fixtures/mazerunnerspringboot/gradlew.bat deleted file mode 100644 index e95643d6..00000000 --- a/features/fixtures/mazerunnerspringboot/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java deleted file mode 100644 index a7425c16..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.bugsnag.mazerunner.scenarios; - -import com.bugsnag.Bugsnag; -import org.springframework.web.client.RestTemplate; - -/** - * Causes an unhandled exception in an async method - */ -public class AsyncMethodScenario extends Scenario { - - public AsyncMethodScenario(Bugsnag bugsnag) { - super(bugsnag); - } - - @Override - public void run() { - - // Don't report any sessions during this test - disableSessionDelivery(); - - // The rest endpoint will run an async task to throw the exception - final String uri = "http://localhost:1234/run-async-task"; - - try { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getForObject(uri, String.class); - - // Wait for the async task to complete - Thread.sleep(2000); - } catch (Exception ex) { - // ignore - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java deleted file mode 100644 index 2e1d3c1f..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.bugsnag.mazerunner.scenarios; - -import com.bugsnag.Bugsnag; -import org.springframework.web.client.RestTemplate; - -/** - * Notifies from an async method - */ -public class AsyncNotifyScenario extends Scenario { - - public AsyncNotifyScenario(Bugsnag bugsnag) { - super(bugsnag); - } - - @Override - public void run() { - - // Don't report any sessions during this test - disableSessionDelivery(); - - // The rest endpoint will run an async task to throw the exception - final String uri = "http://localhost:1234/notify-async-task"; - - try { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getForObject(uri, String.class); - - // Wait for the async task to complete - Thread.sleep(2000); - } catch (Exception ex) { - // ignore - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java deleted file mode 100644 index 8058edd9..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.bugsnag.mazerunner.scenarios; - -import com.bugsnag.Bugsnag; -import org.springframework.web.client.RestTemplate; - -/** - * Causes an unhandled exception in the rest controller - */ -public class AutoSessionScenario extends Scenario { - - public AutoSessionScenario(Bugsnag bugsnag) { - super(bugsnag); - } - - @Override - public void run() { - bugsnag.setAutoCaptureSessions(true); - - final String uri = "http://localhost:1234/add-session"; - - try { - RestTemplate restTemplate = new RestTemplate(); - String result = restTemplate.getForObject(uri, String.class); - LOGGER.info("Completed auto session request: " + result); - Thread.sleep(2000); - } catch (Exception ex) { - LOGGER.error("Failed to complete request", ex); - } - - flushAllSessions(); - - try { - Thread.sleep(2000); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java deleted file mode 100644 index c9ce5da6..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bugsnag.mazerunner.scenarios; - -import com.bugsnag.Bugsnag; -import org.springframework.web.client.RestTemplate; - -/** - * Causes an unhandled exception in the rest controller - */ -public class RestControllerScenario extends Scenario { - - public RestControllerScenario(Bugsnag bugsnag) { - super(bugsnag); - } - - @Override - public void run() { - // Don't report any sessions during this test - disableSessionDelivery(); - - final String uri = "http://localhost:1234/send-unhandled-exception"; - - try { - RestTemplate restTemplate = new RestTemplate(); - String result = restTemplate.getForObject(uri, String.class); - } catch (Exception ex) { - // ignore - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java deleted file mode 100644 index 5dc877b2..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.bugsnag.mazerunner.scenarios; - -import com.bugsnag.Bugsnag; -import com.bugsnag.Report; -import com.bugsnag.callbacks.Callback; -import com.bugsnag.mazerunnerspringboot.ScheduledTaskExecutorService; -import java.util.Collection; - -public class ScheduledTaskExecutorScenario extends Scenario { - - public ScheduledTaskExecutorScenario(Bugsnag bugsnag) { - super(bugsnag); - } - - @Override - public void run() { - // Enable throwing an exception in the scheduled task - ScheduledTaskExecutorService.setSendException(); - - // Wait for the exception - try { - Thread.sleep(5000); - } catch (InterruptedException ex) { - // ignore - } - - final Collection threadnames = ScheduledTaskExecutorService.getThreadNames(); - bugsnag.notify(new RuntimeException("Whoops"), new Callback() { - @Override - public void beforeNotify(Report report) { - report.addToTab("executor", "multiThreaded", threadnames.size() > 1); - report.addToTab("executor", "names", threadnames); - } - }); - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java deleted file mode 100644 index acf29628..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.bugsnag.mazerunner.scenarios; - -import com.bugsnag.Bugsnag; -import com.bugsnag.mazerunnerspringboot.ScheduledTaskService; - -/** - * Causes an unhandled exception in a scheduled task - */ -public class ScheduledTaskScenario extends Scenario { - - public ScheduledTaskScenario(Bugsnag bugsnag) { - super(bugsnag); - } - - @Override - public void run() { - // Enable throwing an exception in the scheduled task - ScheduledTaskService.setThrowException(); - - // Wait for the exception - try { - Thread.sleep(5000); - } catch (InterruptedException ex) { - // ignore - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java deleted file mode 100644 index aa57b959..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; - -/** - * Kicks off the Spring Boot application. - */ -@SpringBootApplication -@EnableScheduling -@EnableAsync -public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java deleted file mode 100644 index 5e0593a0..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -public class AsyncMethodService { - - @Autowired - Bugsnag bugsnag; - - @Async - public void doSomethingAsync() { - - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); - - try { - Thread.sleep(100); - } catch (InterruptedException ex) { - // ignore - } - - throw new RuntimeException("Unhandled exception from Async method"); - } - - @Async - public void notifyAsync() { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "inAsyncMethod", "meta data from async method"); - - bugsnag.notify(new RuntimeException("test from async")); - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java deleted file mode 100644 index 3c05e2a6..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; -import com.bugsnag.BugsnagAsyncExceptionHandler; -import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.AsyncConfigurerSupport; -import org.springframework.scheduling.annotation.EnableScheduling; - -@Configuration -@EnableScheduling -public class BugsnagAsyncConfig extends AsyncConfigurerSupport { - @Autowired - private Bugsnag bugsnag; - - @Override - public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { - return new BugsnagAsyncExceptionHandler(bugsnag); - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java deleted file mode 100644 index 35b7d45d..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; -import com.bugsnag.BugsnagSpringConfiguration; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import(BugsnagSpringConfiguration.class) -public class Config { - - @Value("${BUGSNAG_API_KEY}") - private String bugsnagApiKey; - - @Value("${MAZERUNNER_BASE_URL}") - private String bugsnagEndpoint; - - @Bean - public Bugsnag bugsnag() { - Bugsnag bugsnag = new Bugsnag(bugsnagApiKey); - bugsnag.setEndpoints(bugsnagEndpoint + "notify", bugsnagEndpoint + "sessions"); - bugsnag.setReleaseStage("production"); - bugsnag.setAppVersion("1.0.0"); - return bugsnag; - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java deleted file mode 100644 index 532bbc77..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.scheduling.annotation.EnableScheduling; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -@Configuration -@EnableScheduling -public class ScheduledTaskConfig { - - @ConditionalOnProperty(name = "scheduled_executor_service_bean", havingValue = "true") - @Bean - public Executor taskScheduler() { - return Executors.newScheduledThreadPool(4); - } - - @ConditionalOnProperty(name = "other_scheduled_executor_service_bean", havingValue = "true") - @Bean - public Executor otherTaskScheduler() { - return Executors.newScheduledThreadPool(2); - } - - @ConditionalOnProperty(name = "custom_task_scheduler_bean", havingValue = "true") - @Bean - public TaskScheduler customTaskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setPoolSize(4); - return scheduler; - } - - @ConditionalOnProperty(name = "second_task_scheduler_bean", havingValue = "true") - @Bean(name = "taskScheduler") - public TaskScheduler secondTaskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setPoolSize(2); - return scheduler; - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java deleted file mode 100644 index 99c3a988..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.beans.factory.annotation.Autowired; -import java.util.HashSet; -import java.util.Set; -import java.util.Collection; - -@Service -public class ScheduledTaskExecutorService { - - @Autowired - private Bugsnag bugsnag; - - private final Set threadNames = new HashSet(); - - private volatile boolean sendException = false; - - private static ScheduledTaskExecutorService instance; - - public ScheduledTaskExecutorService() { - instance = this; - } - - public static void setSendException() { - instance.sendException = true; - } - - public static Collection getThreadNames() { - return new HashSet<>(instance.threadNames); - } - - @Scheduled(fixedRate = 100) - public void doSomething() { - if (sendException) { - threadNames.add(Thread.currentThread().getName()); - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java deleted file mode 100644 index 93cb9d6c..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -@Service -public class ScheduledTaskService { - - private volatile boolean throwException = false; - - private static ScheduledTaskService instance; - - public ScheduledTaskService() { - instance = this; - } - - public static void setThrowException() { - instance.throwException = true; - } - - @Scheduled(fixedDelay = 3000) - public void doSomething() { - if (throwException) { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); - - throw new RuntimeException("Unhandled exception from ScheduledTaskService"); - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java deleted file mode 100644 index 82839269..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; -import com.bugsnag.mazerunner.scenarios.Scenario; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.ExitCodeGenerator; -import org.springframework.boot.SpringApplication; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Constructor; - -@Component -public class TestCaseRunner implements CommandLineRunner, ApplicationContextAware { - - private static final Logger LOGGER = LoggerFactory.getLogger(TestCaseRunner.class); - - private ApplicationContext ctx; - - @Autowired - private Bugsnag bugsnag; - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - ctx = applicationContext; - } - - @Override - public void run(String... args) { - // Create and run the test case - LOGGER.info("Creating test case"); - String type = System.getenv("EVENT_TYPE"); - Scenario scenario = testCaseForName(type); - if (scenario != null) { - LOGGER.info("running test case " + type); - scenario.run(); - } else { - LOGGER.error("No test case found for " + type); - } - - // Exit the application - LOGGER.info("Exiting spring"); - System.exit(SpringApplication.exit(ctx, (ExitCodeGenerator) new ExitCodeGenerator() { - @Override - public int getExitCode() { - return 0; - } - })); - } - - private Scenario testCaseForName(String eventType) { - try { - Class clz = Class.forName("com.bugsnag.mazerunner.scenarios." + eventType); - Constructor constructor = clz.getConstructors()[0]; - return (Scenario) constructor.newInstance(bugsnag); - } catch (Exception ex) { - LOGGER.error("Error getting scenario", ex); - return null; - } - } -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java b/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java deleted file mode 100644 index e35c0524..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.bugsnag.mazerunnerspringboot; - -import com.bugsnag.Bugsnag; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class TestRestController { - - private static final Logger LOGGER = LoggerFactory.getLogger(TestRestController.class); - - @Autowired - Bugsnag bugsnag; - - @Autowired - private AsyncMethodService asyncMethodService; - - private static TestRestController instance; - - public TestRestController() { - instance = this; - } - - public static Bugsnag getBugsnag() { - return instance.bugsnag; - } - - @RequestMapping("/send-unhandled-exception") - public String sendUnhandledException() { - throw new RuntimeException("Unhandled exception from TestRestController"); - } - - @RequestMapping("/add-session") - public String addSession() { - // A session should be automatically recorded by Bugsnag if automatic sessions are enabled - LOGGER.info("Starting a new session"); - return ""; - } - - @RequestMapping("/run-async-task") - public String runAsyncTask() { - try { - asyncMethodService.doSomethingAsync(); - } catch (Exception ex) { - // This should not happen - LOGGER.info("Saw exception from async call"); - } - - return ""; - } - - @RequestMapping("/notify-async-task") - public String notifyAsyncTask() { - - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "controllerMethod", "meta data from controller method"); - - // Notify before calling the async method - bugsnag.notify(new RuntimeException("test from before async")); - - // Call the async method (also notifies) - asyncMethodService.notifyAsync(); - - return ""; - } - -} diff --git a/features/fixtures/mazerunnerspringboot/src/main/resources/application.properties b/features/fixtures/mazerunnerspringboot/src/main/resources/application.properties deleted file mode 100644 index 7e96c7f7..00000000 --- a/features/fixtures/mazerunnerspringboot/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -server.port=1234 diff --git a/features/fixtures/mazerunnerspringboot3/build.gradle b/features/fixtures/mazerunnerspringboot3/build.gradle index 77963142..2d7863ed 100644 --- a/features/fixtures/mazerunnerspringboot3/build.gradle +++ b/features/fixtures/mazerunnerspringboot3/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '3.0.0' + springBootVersion = '3.5.6' } repositories { mavenCentral() @@ -17,6 +17,8 @@ apply plugin: 'io.spring.dependency-management' group 'com.bugsnag.mazerunnerspringboot3' version '1.0-SNAPSHOT' +sourceCompatibility = '17' + repositories { mavenCentral() maven { @@ -27,13 +29,15 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'ch.qos.logback:logback-classic:1.5.18' + implementation 'org.codehaus.janino:janino:3.1.10' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.14.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1' implementation 'com.bugsnag:bugsnag:9.9.9-test' implementation 'com.bugsnag:bugsnag-spring:9.9.9-test' implementation project(":scenarios") - // required for JDK 9 and above - implementation('javax.xml.bind:jaxb-api:2.3.0') testImplementation group: 'junit', name: 'junit', version: '4.13.2' } \ No newline at end of file diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java index 532bbc77..febf74cb 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java @@ -6,39 +6,39 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.scheduling.annotation.EnableScheduling; -import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; @Configuration -@EnableScheduling public class ScheduledTaskConfig { @ConditionalOnProperty(name = "scheduled_executor_service_bean", havingValue = "true") - @Bean - public Executor taskScheduler() { + @Bean(name = "taskScheduler") + public ScheduledExecutorService taskScheduler() { return Executors.newScheduledThreadPool(4); } @ConditionalOnProperty(name = "other_scheduled_executor_service_bean", havingValue = "true") @Bean - public Executor otherTaskScheduler() { + public ScheduledExecutorService otherTaskScheduler() { return Executors.newScheduledThreadPool(2); } @ConditionalOnProperty(name = "custom_task_scheduler_bean", havingValue = "true") - @Bean + @Bean(name = "taskScheduler") public TaskScheduler customTaskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(4); + scheduler.initialize(); return scheduler; } @ConditionalOnProperty(name = "second_task_scheduler_bean", havingValue = "true") - @Bean(name = "taskScheduler") + @Bean(name = "secondTaskScheduler") public TaskScheduler secondTaskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(2); + scheduler.initialize(); return scheduler; } } diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java index 93cb9d6c..db4937ec 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java @@ -22,6 +22,9 @@ public static void setThrowException() { @Scheduled(fixedDelay = 3000) public void doSomething() { if (throwException) { + // Reset the flag so we only throw once + throwException = false; + // Add some thread meta data Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); Bugsnag.clearThreadMetaData(); @@ -31,3 +34,4 @@ public void doSomething() { } } } + diff --git a/features/fixtures/scenarios/build.gradle b/features/fixtures/scenarios/build.gradle index c7074b7b..276da29f 100644 --- a/features/fixtures/scenarios/build.gradle +++ b/features/fixtures/scenarios/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' group 'com.bugsnag.mazerunner' -sourceCompatibility = '1.8' +sourceCompatibility = '17' repositories { mavenCentral() @@ -12,7 +12,7 @@ repositories { } dependencies { - implementation("ch.qos.logback:logback-classic:1.2.3") + implementation("ch.qos.logback:logback-classic:1.5.18") + implementation("org.codehaus.janino:janino:3.1.10") implementation("com.bugsnag:bugsnag:9.9.9-test") } - diff --git a/features/fixtures/settings.gradle b/features/fixtures/settings.gradle index 3a78b3ca..6a101a85 100644 --- a/features/fixtures/settings.gradle +++ b/features/fixtures/settings.gradle @@ -1,13 +1,7 @@ include ':mazerunner', - ':mazerunnerplainspring', - ':mazerunnerspringboot', ':scenarios' -try { - if (JavaVersion.current() >= JavaVersion.VERSION_17) { - include ':mazerunnerspringboot3', - ':mazerunnerplainspring6' - } -} catch (ignored) { - // ignore any errors, they are almost always Gradle / JVM version issues +if (JavaVersion.current() >= JavaVersion.VERSION_17) { + include ':mazerunnerspringboot3', + ':mazerunnerplainspring6' } diff --git a/features/project_package.feature b/features/project_package.feature index e2dbc6c5..348f4858 100644 --- a/features/project_package.feature +++ b/features/project_package.feature @@ -13,7 +13,7 @@ Scenario: Test logback appender with no project packages And the event "exceptions.0.stacktrace.1.inProject" is false And the event "exceptions.0.stacktrace.2.method" equals "com.bugsnag.mazerunner.TestCaseRunner.run" And the event "exceptions.0.stacktrace.2.inProject" is false - And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.callRunner" + And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.lambda$callRunner$5" And the event "exceptions.0.stacktrace.3.inProject" is false Scenario: Test logback appender with a project package "com.bugsnag.mazerunner" defined @@ -29,7 +29,7 @@ Scenario: Test logback appender with a project package "com.bugsnag.mazerunner" And the event "exceptions.0.stacktrace.1.inProject" is true And the event "exceptions.0.stacktrace.2.method" equals "com.bugsnag.mazerunner.TestCaseRunner.run" And the event "exceptions.0.stacktrace.2.inProject" is true - And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.callRunner" + And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.lambda$callRunner$5" And the event "exceptions.0.stacktrace.3.inProject" is false Scenario: Test plain Java app with no project packages @@ -44,7 +44,7 @@ Scenario: Test plain Java app with no project packages And the event "exceptions.0.stacktrace.1.inProject" is false And the event "exceptions.0.stacktrace.2.method" equals "com.bugsnag.mazerunner.TestCaseRunner.run" And the event "exceptions.0.stacktrace.2.inProject" is false - And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.callRunner" + And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.lambda$callRunner$5" And the event "exceptions.0.stacktrace.3.inProject" is false Scenario: Test plain Java app with a project package "com.bugsnag.mazerunner" defined @@ -59,5 +59,5 @@ Scenario: Test plain Java app with a project package "com.bugsnag.mazerunner" de And the event "exceptions.0.stacktrace.1.inProject" is true And the event "exceptions.0.stacktrace.2.method" equals "com.bugsnag.mazerunner.TestCaseRunner.run" And the event "exceptions.0.stacktrace.2.inProject" is true - And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.callRunner" + And the event "exceptions.0.stacktrace.3.method" equals "org.springframework.boot.SpringApplication.lambda$callRunner$5" And the event "exceptions.0.stacktrace.3.inProject" is false diff --git a/features/scripts/build-plain-spring-app.sh b/features/scripts/build-plain-spring-app.sh index 12c65cd6..4a8f751d 100755 --- a/features/scripts/build-plain-spring-app.sh +++ b/features/scripts/build-plain-spring-app.sh @@ -2,10 +2,5 @@ # Start Tomcat then copy a WAR file across to serve it catalina.sh start -if [[ "${JAVA_VERSION}" == "8"* ]]; then - ./gradlew -p features/fixtures/mazerunnerplainspring war - cp features/fixtures/mazerunnerplainspring/build/libs/mazerunnerplainspring.war $CATALINA_HOME/webapps/ROOT.war -else - ./gradlew -p features/fixtures/mazerunnerplainspring6 war - cp features/fixtures/mazerunnerplainspring6/build/libs/mazerunnerplainspring.war $CATALINA_HOME/webapps/ROOT.war -fi \ No newline at end of file +./gradlew -p features/fixtures/mazerunnerplainspring6 war +cp features/fixtures/mazerunnerplainspring6/build/libs/mazerunnerplainspring.war $CATALINA_HOME/webapps/ROOT.war diff --git a/features/scripts/run-java-spring-boot-app.sh b/features/scripts/run-java-spring-boot-app.sh index 378dc467..239f1131 100755 --- a/features/scripts/run-java-spring-boot-app.sh +++ b/features/scripts/run-java-spring-boot-app.sh @@ -1,9 +1,4 @@ #!/usr/bin/env bash catalina.sh stop -if [[ "${JAVA_VERSION}" == "8"* ]]; then - cd features/fixtures/mazerunnerspringboot - ./gradlew bootRun -else - ./gradlew -p features/fixtures/mazerunnerspringboot3 bootRun -fi \ No newline at end of file +./gradlew -p features/fixtures/mazerunnerspringboot3 bootRun \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 05376a7d..4e3b050f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip diff --git a/release.gradle b/release.gradle index 4cb9e10d..975476fc 100644 --- a/release.gradle +++ b/release.gradle @@ -1,42 +1,71 @@ +// release.gradle — script plugin (apply from: ...) + +apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'signing' -archivesBaseName = project.findProperty('artifactId') +/** -------- Helpers -------- */ +def projectProperty = { String k, def d = null -> project.findProperty(k) ?: d } // gradle property (no envs) +def projectBoolean = { String k -> (projectProperty(k)?.toString()?.toBoolean() ?: false) } + +def isRelease = projectBoolean('releasing') // set with -Preleasing=true on real deploys + +// Prefer Gradle properties (works on CI without env vars). +// Put these in ~/.gradle/gradle.properties or project gradle.properties on CI. +def ossrhUser = projectProperty('NEXUS_USERNAME') ?: projectProperty('ossrhUsername') +def ossrhPass = projectProperty('NEXUS_PASSWORD') ?: projectProperty('ossrhPassword') -tasks.maybeCreate('sourceJar', Jar).configure { - from java.sourceSets.main.allJava +// Optional in-memory signing keys (recommended for CI) +// signingKey: ASCII-armored private key, signingPassword: key passphrase +def signingKey = projectProperty('signingKey') +def signingPassword = projectProperty('signingPassword') + +/** -------- Java / Artifacts -------- */ +base { + archivesName = projectProperty('artifactId') ?: project.name } -publishing { +java { + toolchain { languageVersion = JavaLanguageVersion.of(17) } + withSourcesJar() + withJavadocJar() +} +/** -------- Publishing -------- */ +publishing { repositories { + // Always-available local test repo maven { - name = 'ossrhStaging' - url = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" - credentials { - username = project.hasProperty("NEXUS_USERNAME") ? "$NEXUS_USERNAME" : System.getenv("NEXUS_USERNAME") - password = project.hasProperty("NEXUS_PASSWORD") ? "$NEXUS_PASSWORD" : System.getenv("NEXUS_PASSWORD") - } + name = 'test' + url = uri(rootProject.file('build/repository')) } - maven { - name 'test' - url rootProject.file('build/repository').toURI().toString() + // OSSRH only if credentials are available (no envs needed) + if (ossrhUser && ossrhPass) { + maven { + name = 'ossrhStaging' + url = uri('https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/') + credentials { + username = ossrhUser + password = ossrhPass + } + } + } else { + logger.lifecycle("[publishing] OSSRH credentials not provided; skipping remote repository configuration.") } } - + publications { - Publication(MavenPublication) { - from project(':' + project.name).components.java - groupId 'com.bugsnag' - artifactId project.findProperty('artifactId') - version rootProject.version - artifact sourceJar { - classifier "sources" - } + mavenJava(MavenPublication) { + from components.java + + groupId = 'com.bugsnag' + artifactId = projectProperty('artifactId') ?: project.name + version = rootProject.version + pom { - name = project.findProperty('projectName') - description = project.findProperty('projectDescription') + name = projectProperty('projectName') ?: artifactId + description = projectProperty('projectDescription') ?: "Bugsnag Java Notifier" url = 'https://github.com/bugsnag/bugsnag-java' scm { @@ -68,8 +97,28 @@ publishing { } } } +} - signing { - sign publishing.publications +/** -------- Signing (no .asc on test publishes) -------- */ +signing { + // Only *require* signing for real releases + required { isRelease } + + // Only attach signatures when we actually want to sign + if (isRelease) { + if (signingKey && signingPassword) { + useInMemoryPgpKeys(signingKey as String, signingPassword as String) + } + sign(publishing.publications['mavenJava']) } } + +/** -------- Safety: never try publishing to OSSRH without creds -------- */ +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository + +tasks.withType(PublishToMavenRepository).configureEach { + onlyIf { + // Allow all "test" publishes, and only allow OSSRH when creds exist + repository?.name != 'ossrhStaging' || (ossrhUser && ossrhPass) + } +} \ No newline at end of file diff --git a/scripts/build-test-repository.sh b/scripts/build-test-repository.sh index 948547fe..1a01ed87 100755 --- a/scripts/build-test-repository.sh +++ b/scripts/build-test-repository.sh @@ -1,4 +1,11 @@ #!/usr/bin/env bash -./gradlew -xsignPublicationPublication -Preleasing=true -Pversion=9.9.9-test publishPublicationPublicationToTestRepository -cd build/repository/ || exit -zip -r ../../maven-repository.zip ./* +set -euo pipefail +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +./gradlew -Pversion=9.9.9-test publishToTestRepoAll + +REPO_DIR="$ROOT_DIR/build/repository" +mkdir -p "$REPO_DIR" +cd "$REPO_DIR" +zip -r "$ROOT_DIR/maven-repository.zip" ./* \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 596924ce..9c8a3d8f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,6 @@ include ':bugsnag', ':bugsnag-spring' include ':examples:simple', - ':examples:servlet-javax', ':examples:spring', ':examples:spring-web', ':examples:logback' From 8aa75805f95bbf5f04350e28beba739b68d22c4d Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Fri, 24 Oct 2025 15:12:13 +0200 Subject: [PATCH 02/10] Rename filters to redacted keys for spec compliance (#236) --- .../src/main/java/com/bugsnag/Bugsnag.java | 10 +- .../java/com/bugsnag/BugsnagAppender.java | 24 ++-- .../main/java/com/bugsnag/Configuration.java | 2 +- .../FilteredMap.java => RedactedMap.java} | 63 ++++++----- bugsnag/src/main/java/com/bugsnag/Report.java | 6 +- .../test/java/com/bugsnag/AppenderTest.java | 26 ++--- .../test/java/com/bugsnag/BugsnagTest.java | 26 ++--- .../bugsnag/JakartaServletCallbackTest.java | 4 +- ...teredMapTest.java => RedactedMapTest.java} | 106 +++++++++--------- bugsnag/src/test/resources/logback.xml | 4 +- .../logback/src/main/resources/logback.xml | 4 +- features/filtering_metadata.feature | 54 --------- .../logback/meta_data_filter_config.xml | 18 --- .../meta_data_redact_config.xml} | 5 +- ...rScenario.java => AutoRedactScenario.java} | 6 +- ...cenario.java => ManualRedactScenario.java} | 8 +- features/redacting_metadata.feature | 55 +++++++++ features/scripts/assemble-fixtures.sh | 12 +- 18 files changed, 211 insertions(+), 222 deletions(-) rename bugsnag/src/main/java/com/bugsnag/{util/FilteredMap.java => RedactedMap.java} (55%) rename bugsnag/src/test/java/com/bugsnag/{util/FilteredMapTest.java => RedactedMapTest.java} (50%) delete mode 100644 features/filtering_metadata.feature delete mode 100644 features/fixtures/logback/meta_data_filter_config.xml rename features/fixtures/{mazerunner/src/main/resources/logback.xml => logback/meta_data_redact_config.xml} (93%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{AutoFilterScenario.java => AutoRedactScenario.java} (83%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{ManualFilterScenario.java => ManualRedactScenario.java} (78%) create mode 100644 features/redacting_metadata.feature diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index abfaffd0..697e433d 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -228,16 +228,16 @@ public void setEndpoint(String endpoint) { } /** - * Set which keys should be filtered when sending metaData to Bugsnag. + * Set which keys should be redacted when sending metaData to Bugsnag. * Use this when you want to ensure sensitive information, such as passwords * or credit card information is stripped from metaData you send to Bugsnag. * Any keys in metaData which contain these strings will be marked as - * [FILTERED] when send to Bugsnag. + * [REDACTED] when send to Bugsnag. * - * @param filters a list of String keys to filter from metaData + * @param redactedKeys a list of String keys to redact from metaData */ - public void setFilters(String... filters) { - config.filters = filters; + public void setRedactedKeys(String... redactedKeys) { + config.redactedKeys = redactedKeys; } /** diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 5d3cd23d..74f890ca 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -48,8 +48,8 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase { /** Bugsnag error server endpoint. */ private String endpoint; - /** Property names that should be filtered out before sending to Bugsnag servers. */ - private Set filteredProperties = new HashSet(); + /** Property names that should be redacted before sending to Bugsnag servers. */ + private Set redactedKeys = new HashSet(); /** Exception classes to be ignored. */ private Set ignoredClasses = new HashSet(); @@ -254,8 +254,8 @@ private Bugsnag createBugsnag() { bugsnag.setTimeout(timeout); } - if (filteredProperties.size() > 0) { - bugsnag.setFilters(filteredProperties.toArray(new String[0])); + if (redactedKeys.size() > 0) { + bugsnag.setRedactedKeys(redactedKeys.toArray(new String[0])); } bugsnag.setIgnoreClasses(ignoredClasses.toArray(new String[0])); @@ -374,24 +374,24 @@ public void setEndpoint(String endpoint) { } /** - * @see Bugsnag#setFilters(String...) + * @see Bugsnag#setRedactedKeys(String...) */ - public void setFilteredProperty(String filter) { - this.filteredProperties.add(filter); + public void setRedactedKey(String key) { + this.redactedKeys.add(key); if (bugsnag != null) { - bugsnag.setFilters(this.filteredProperties.toArray(new String[0])); + bugsnag.setRedactedKeys(this.redactedKeys.toArray(new String[0])); } } /** - * @see Bugsnag#setFilters(String...) + * @see Bugsnag#setRedactedKeys(String...) */ - public void setFilteredProperties(String filters) { - this.filteredProperties.addAll(split(filters)); + public void setRedactedKeys(String key) { + this.redactedKeys.addAll(split(key)); if (bugsnag != null) { - bugsnag.setFilters(this.filteredProperties.toArray(new String[0])); + bugsnag.setRedactedKeys(this.redactedKeys.toArray(new String[0])); } } diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index a55828e4..c3b5c7d4 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -36,7 +36,7 @@ public class Configuration { public Delivery delivery; public EndpointConfiguration endpointConfiguration; public Delivery sessionDelivery; - public String[] filters = new String[]{"password", "secret", "Authorization", "Cookie"}; + public String[] redactedKeys = new String[]{"password", "secret", "Authorization", "Cookie"}; public String[] ignoreClasses; public String[] notifyReleaseStages = null; public String[] projectPackages; diff --git a/bugsnag/src/main/java/com/bugsnag/util/FilteredMap.java b/bugsnag/src/main/java/com/bugsnag/RedactedMap.java similarity index 55% rename from bugsnag/src/main/java/com/bugsnag/util/FilteredMap.java rename to bugsnag/src/main/java/com/bugsnag/RedactedMap.java index 1adc9269..b417137d 100644 --- a/bugsnag/src/main/java/com/bugsnag/util/FilteredMap.java +++ b/bugsnag/src/main/java/com/bugsnag/RedactedMap.java @@ -1,24 +1,24 @@ -package com.bugsnag.util; +package com.bugsnag; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; /** - * Decorates a map by replacing values of filtered keys. + * Decorates a map by replacing values of redacted keys. */ -public class FilteredMap implements Map { +class RedactedMap implements Map { - private static final String FILTERED_PLACEHOLDER = "[FILTERED]"; + private static final String REDACTED_PLACEHOLDER = "[REDACTED]"; - private final Map filteredCopy; - private final Collection keyFilters = new ArrayList(); + private final Map redactedCopy; + private final Collection redactedKeys = new HashSet<>(); - public FilteredMap(Map map, Collection keyFilters) { - this.keyFilters.addAll(keyFilters); - this.filteredCopy = createCopy(map); + RedactedMap(Map map, Collection redactedKeys) { + this.redactedKeys.addAll(redactedKeys); + this.redactedCopy = createCopy(map); } private Map createCopy(Map map) { @@ -36,84 +36,87 @@ private Map createCopy(Map map) { @Override public int size() { - return filteredCopy.size(); + return redactedCopy.size(); } @Override public boolean isEmpty() { - return filteredCopy.isEmpty(); + return redactedCopy.isEmpty(); } @Override public boolean containsKey(Object key) { - return filteredCopy.containsKey(key); + return redactedCopy.containsKey(key); } @Override public boolean containsValue(Object value) { - return filteredCopy.containsValue(value); + return redactedCopy.containsValue(value); } @Override public Object get(Object key) { - return filteredCopy.get(key); + return redactedCopy.get(key); } @Override public Object put(String key, Object value) { if (value == null) { - return filteredCopy.put(key, null); + return redactedCopy.put(key, null); } Object transformedValue = transformEntry(key, value); - return filteredCopy.put(key, transformedValue); + return redactedCopy.put(key, transformedValue); } @Override public Object remove(Object key) { - return filteredCopy.remove(key); + return redactedCopy.remove(key); } @Override public void putAll(Map mapValues) { Map copy = createCopy(mapValues); - filteredCopy.putAll(copy); + redactedCopy.putAll(copy); } @Override public void clear() { - filteredCopy.clear(); + redactedCopy.clear(); } @Override public Set keySet() { - return filteredCopy.keySet(); + return redactedCopy.keySet(); } @Override public Collection values() { - return filteredCopy.values(); + return redactedCopy.values(); } @Override public Set> entrySet() { - return filteredCopy.entrySet(); + return redactedCopy.entrySet(); } @SuppressWarnings("unchecked") private Object transformEntry(Object key, Object value) { if (value instanceof Map) { - return new FilteredMap((Map) value, keyFilters); + return new RedactedMap((Map) value, redactedKeys); } - return shouldFilterKey((String) key) ? FILTERED_PLACEHOLDER : value; + return shouldRedactKey((String) key) ? REDACTED_PLACEHOLDER : value; } - private boolean shouldFilterKey(String key) { - if (keyFilters == null || key == null) { + private boolean shouldRedactKey(String key) { + if (key == null) { return false; } - - for (String filter : keyFilters) { - if (key.contains(filter)) { + // Check for common keys first before looping through all + if (redactedKeys.contains(key)) { + return true; + } + for (String redactedKey : redactedKeys) { + if (key.contains(redactedKey)) { return true; } } diff --git a/bugsnag/src/main/java/com/bugsnag/Report.java b/bugsnag/src/main/java/com/bugsnag/Report.java index dd1527e5..baba0569 100644 --- a/bugsnag/src/main/java/com/bugsnag/Report.java +++ b/bugsnag/src/main/java/com/bugsnag/Report.java @@ -2,13 +2,11 @@ import com.bugsnag.serialization.Expose; -import com.bugsnag.util.FilteredMap; - import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; public class Report { @@ -126,7 +124,7 @@ public Map getUser() { @Expose public Map getMetaData() { - return new FilteredMap(diagnostics.metaData, Arrays.asList(config.filters)); + return new RedactedMap(diagnostics.metaData, Set.of(config.redactedKeys)); } @Expose diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index 19c9ad42..0d7a56f0 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -144,10 +144,10 @@ public void testBugsnagConfig() { assertEquals("gradleTask", config.appType); assertFalse(config.shouldAutoCaptureSessions()); - assertEquals(2, config.filters.length); - ArrayList filters = new ArrayList(Arrays.asList(config.filters)); - assertTrue(filters.contains("password")); - assertTrue(filters.contains("credit_card_number")); + assertEquals(2, config.redactedKeys.length); + ArrayList redactedKeys = new ArrayList(Arrays.asList(config.redactedKeys)); + assertTrue(redactedKeys.contains("password")); + assertTrue(redactedKeys.contains("credit_card_number")); assertEquals(2, config.ignoreClasses.length); ArrayList ignoreClasses @@ -274,15 +274,15 @@ public void testAppType() { } @Test - public void testFilters() { + public void testRedactedKeys() { - // Add some meta data which should be filtered by key name + // Add some metadata which should be redacted by key name Bugsnag.addThreadMetaData("myTab", "password", "password value"); Bugsnag.addThreadMetaData("myTab", "credit_card_number", "card number"); - Bugsnag.addThreadMetaData("myTab", "mysecret", "not filtered"); + Bugsnag.addThreadMetaData("myTab", "mysecret", "not redacted"); // Send a log message - LOGGER.warn("Exception with filtered meta data", new RuntimeException("test")); + LOGGER.warn("Exception with redacted meta data", new RuntimeException("test")); // Check that a report was sent to Bugsnag assertEquals(1, delivery.getNotifications().size()); @@ -291,9 +291,9 @@ public void testFilters() { assertTrue(notification.getEvents().get(0).getMetaData().containsKey("myTab")); Map myTab = getMetaDataMap(notification, "myTab"); - assertEquals("[FILTERED]", myTab.get("password")); - assertEquals("[FILTERED]", myTab.get("credit_card_number")); - assertEquals("not filtered", myTab.get("mysecret")); + assertEquals("[REDACTED]", myTab.get("password")); + assertEquals("[REDACTED]", myTab.get("credit_card_number")); + assertEquals("not redacted", myTab.get("mysecret")); } @Test @@ -316,7 +316,7 @@ public void beforeNotify(Report report) { }); // Send a log message - LOGGER.warn("Exception with filtered meta data", new RuntimeException("test")); + LOGGER.warn("Exception with redacted meta data", new RuntimeException("test")); // Check that a report was sent to Bugsnag assertEquals(1, delivery.getNotifications().size()); @@ -412,7 +412,7 @@ private SessionTracker getSessionTracker(Bugsnag bugsnag) { } /** - * Gets a hashmap key from the meta data in a notification + * Gets a hashmap key from the metadata in a notification * * @param notification The notification * @param key The key to get diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index a0308425..0884fc72 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -164,8 +164,8 @@ public void close() { } @Test - public void testFilters() { - bugsnag.setFilters("testfilter1", "testfilter2"); + public void testRedactedKeys() { + bugsnag.setRedactedKeys("testredact1", "testredact2"); bugsnag.setDelivery(new Delivery() { @SuppressWarnings("unchecked") @Override @@ -175,10 +175,10 @@ public void deliver(Serializer serializer, Object object, Map he (Map) report.getMetaData().get("firsttab"); final Map secondTab = (Map) report.getMetaData().get("secondtab"); - assertEquals("[FILTERED]", firstTab.get("testfilter1")); - assertEquals("[FILTERED]", firstTab.get("testfilter2")); - assertEquals("secretpassword", firstTab.get("testfilter3")); - assertEquals("[FILTERED]", secondTab.get("testfilter1")); + assertEquals("[REDACTED]", firstTab.get("testredact1")); + assertEquals("[REDACTED]", firstTab.get("testredact2")); + assertEquals("secretpassword", firstTab.get("testredact3")); + assertEquals("[REDACTED]", secondTab.get("testredact1")); } @Override @@ -188,16 +188,16 @@ public void close() { assertTrue(bugsnag.notify(new Throwable(), new Callback() { @Override public void beforeNotify(Report report) { - report.addToTab("firsttab", "testfilter1", "secretpassword"); - report.addToTab("firsttab", "testfilter2", "secretpassword"); - report.addToTab("firsttab", "testfilter3", "secretpassword"); - report.addToTab("secondtab", "testfilter1", "secretpassword"); + report.addToTab("firsttab", "testredact1", "secretpassword"); + report.addToTab("firsttab", "testredact2", "secretpassword"); + report.addToTab("firsttab", "testredact3", "secretpassword"); + report.addToTab("secondtab", "testredact1", "secretpassword"); } })); } @Test - public void testFilterHeaders() { + public void testRedactHeaders() { bugsnag.setDelivery(new Delivery() { @SuppressWarnings("unchecked") @Override @@ -209,9 +209,9 @@ public void deliver(Serializer serializer, Object object, Map he Map headersMap = (Map) requestTab.get("headers"); - assertEquals("[FILTERED]", headersMap.get("Authorization")); + assertEquals("[REDACTED]", headersMap.get("Authorization")); assertEquals("User:Password", headersMap.get("authorization")); - assertEquals("[FILTERED]", headersMap.get("Cookie")); + assertEquals("[REDACTED]", headersMap.get("Cookie")); assertEquals("123456ABCDEF", headersMap.get("cookie")); } diff --git a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java index 5bd78ec4..f5e6677e 100644 --- a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java +++ b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java @@ -100,10 +100,10 @@ public void testRequestMetadataAdded() { assertEquals("some-data-1,some-data-2", headers.get("X-Custom-Header")); // Make sure that actual Authorization header value is not in the report - assertEquals("[FILTERED]", headers.get("Authorization")); + assertEquals("[REDACTED]", headers.get("Authorization")); // Make sure that actual cookies are not in the report - assertEquals("[FILTERED]", headers.get("Cookie")); + assertEquals("[REDACTED]", headers.get("Cookie")); assertTrue(request.containsKey("params")); Map params = (Map) request.get("params"); diff --git a/bugsnag/src/test/java/com/bugsnag/util/FilteredMapTest.java b/bugsnag/src/test/java/com/bugsnag/RedactedMapTest.java similarity index 50% rename from bugsnag/src/test/java/com/bugsnag/util/FilteredMapTest.java rename to bugsnag/src/test/java/com/bugsnag/RedactedMapTest.java index 32bf424c..46cfb5b4 100644 --- a/bugsnag/src/test/java/com/bugsnag/util/FilteredMapTest.java +++ b/bugsnag/src/test/java/com/bugsnag/RedactedMapTest.java @@ -1,4 +1,4 @@ -package com.bugsnag.util; +package com.bugsnag; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -13,132 +13,132 @@ import java.util.Map; import java.util.Set; -public class FilteredMapTest { +public class RedactedMapTest { - private static final String KEY_UNFILTERED = "unfiltered"; - private static final String KEY_FILTERED = "auth"; + private static final String KEY_UNREDACTED = "unredacted"; + private static final String KEY_REDACTED = "auth"; private static final String KEY_NESTED = "nested"; private static final String KEY_UNMODIFIABLE = "unmodifiable"; - private static final String VAL_UNFILTERED = "Foo"; - private static final String VAL_FILTERED = "Bar"; - private static final String PLACEHOLDER_FILTERED = "[FILTERED]"; + private static final String VAL_UNREDACTED = "Foo"; + private static final String VAL_REDACTED = "Bar"; + private static final String PLACEHOLDER_REDACTED = "[REDACTED]"; - private Map filteredMap; + private Map redactedMap; /** - * Creates a map with filtered, unfiltered, and nested values + * Creates a map with redacted, unredacted, and nested values * @throws Exception an exception */ @Before public void setUp() { HashMap map = new HashMap(); - map.put(KEY_UNFILTERED, VAL_UNFILTERED); - map.put(KEY_FILTERED, VAL_FILTERED); + map.put(KEY_UNREDACTED, VAL_UNREDACTED); + map.put(KEY_REDACTED, VAL_REDACTED); HashMap nestedMap = new HashMap(); - nestedMap.put(KEY_UNFILTERED, VAL_UNFILTERED); - nestedMap.put(KEY_FILTERED, VAL_FILTERED); + nestedMap.put(KEY_UNREDACTED, VAL_UNREDACTED); + nestedMap.put(KEY_REDACTED, VAL_REDACTED); map.put(KEY_NESTED, nestedMap); map.put(KEY_UNMODIFIABLE, Collections.unmodifiableMap(nestedMap)); - this.filteredMap = new FilteredMap(map, Collections.singleton(KEY_FILTERED)); + this.redactedMap = new RedactedMap(map, Collections.singleton(KEY_REDACTED)); } @Test public void testSize() { - assertEquals(4, filteredMap.size()); + assertEquals(4, redactedMap.size()); } @Test public void testIsEmpty() { - assertFalse(filteredMap.isEmpty()); + assertFalse(redactedMap.isEmpty()); Map map = Collections.emptyMap(); - FilteredMap emptyMap = new FilteredMap(map, Collections.emptyList()); + RedactedMap emptyMap = new RedactedMap(map, Collections.emptyList()); assertTrue(emptyMap.isEmpty()); } @Test public void testClear() { - assertEquals(4, filteredMap.size()); - filteredMap.clear(); - assertTrue(filteredMap.isEmpty()); + assertEquals(4, redactedMap.size()); + redactedMap.clear(); + assertTrue(redactedMap.isEmpty()); } @Test public void testContainsKey() { - assertTrue(filteredMap.containsKey(KEY_FILTERED)); - assertTrue(filteredMap.containsKey(KEY_UNFILTERED)); - assertTrue(filteredMap.containsKey(KEY_NESTED)); - assertFalse(filteredMap.containsKey("fake")); + assertTrue(redactedMap.containsKey(KEY_REDACTED)); + assertTrue(redactedMap.containsKey(KEY_UNREDACTED)); + assertTrue(redactedMap.containsKey(KEY_NESTED)); + assertFalse(redactedMap.containsKey("fake")); } @Test public void testRemove() { HashMap map = new HashMap(); - map.put(KEY_UNFILTERED, VAL_UNFILTERED); - map.put(KEY_FILTERED, VAL_FILTERED); + map.put(KEY_UNREDACTED, VAL_UNREDACTED); + map.put(KEY_REDACTED, VAL_REDACTED); HashMap emptyMap = new HashMap(); - Set filters = Collections.singleton(KEY_FILTERED); - Map removeMap = new FilteredMap(emptyMap, filters); + Set keys = Collections.singleton(KEY_REDACTED); + Map removeMap = new RedactedMap(emptyMap, keys); removeMap.putAll(map); assertEquals(2, removeMap.size()); - removeMap.remove(KEY_FILTERED); + removeMap.remove(KEY_REDACTED); assertEquals(1, removeMap.size()); - removeMap.remove(KEY_UNFILTERED); + removeMap.remove(KEY_UNREDACTED); assertEquals(0, removeMap.size()); } @Test public void testGet() { - assertEquals(PLACEHOLDER_FILTERED, filteredMap.get(KEY_FILTERED)); - assertEquals(VAL_UNFILTERED, filteredMap.get(KEY_UNFILTERED)); + assertEquals(PLACEHOLDER_REDACTED, redactedMap.get(KEY_REDACTED)); + assertEquals(VAL_UNREDACTED, redactedMap.get(KEY_UNREDACTED)); - Object actual = filteredMap.get(KEY_NESTED); - assertTrue(actual instanceof FilteredMap); + Object actual = redactedMap.get(KEY_NESTED); + assertTrue(actual instanceof RedactedMap); @SuppressWarnings("unchecked") Map nestedMap = (Map) actual; - assertEquals(VAL_UNFILTERED, nestedMap.get(KEY_UNFILTERED)); - assertEquals(PLACEHOLDER_FILTERED, nestedMap.get(KEY_FILTERED)); + assertEquals(VAL_UNREDACTED, nestedMap.get(KEY_UNREDACTED)); + assertEquals(PLACEHOLDER_REDACTED, nestedMap.get(KEY_REDACTED)); } @Test public void testKeySet() { - Set keySet = filteredMap.keySet(); + Set keySet = redactedMap.keySet(); assertEquals(4, keySet.size()); - assertTrue(keySet.contains(KEY_FILTERED)); - assertTrue(keySet.contains(KEY_UNFILTERED)); + assertTrue(keySet.contains(KEY_REDACTED)); + assertTrue(keySet.contains(KEY_UNREDACTED)); assertTrue(keySet.contains(KEY_NESTED)); } @Test public void testValues() { - Collection values = filteredMap.values(); + Collection values = redactedMap.values(); assertEquals(4, values.size()); - assertTrue(values.contains(VAL_UNFILTERED)); - assertTrue(values.contains(PLACEHOLDER_FILTERED)); + assertTrue(values.contains(VAL_UNREDACTED)); + assertTrue(values.contains(PLACEHOLDER_REDACTED)); - values.remove(PLACEHOLDER_FILTERED); - values.remove(VAL_UNFILTERED); + values.remove(PLACEHOLDER_REDACTED); + values.remove(VAL_UNREDACTED); Object nestedObj = values.toArray(new Object[1])[0]; - assertTrue(nestedObj instanceof FilteredMap); + assertTrue(nestedObj instanceof RedactedMap); @SuppressWarnings("unchecked") Map nestedMap = (Map) nestedObj; values = nestedMap.values(); assertEquals(2, values.size()); - assertTrue(values.contains(VAL_UNFILTERED)); - assertTrue(values.contains(PLACEHOLDER_FILTERED)); + assertTrue(values.contains(VAL_UNREDACTED)); + assertTrue(values.contains(PLACEHOLDER_REDACTED)); } @Test public void testEntrySet() { - Set> entries = filteredMap.entrySet(); + Set> entries = redactedMap.entrySet(); assertEquals(4, entries.size()); int expectedCount = 0; @@ -146,18 +146,18 @@ public void testEntrySet() { for (Map.Entry entry : entries) { String key = entry.getKey(); - if (key.equals(KEY_FILTERED)) { + if (key.equals(KEY_REDACTED)) { expectedCount++; - assertEquals(PLACEHOLDER_FILTERED, entry.getValue()); + assertEquals(PLACEHOLDER_REDACTED, entry.getValue()); - } else if (key.equals(KEY_UNFILTERED)) { + } else if (key.equals(KEY_UNREDACTED)) { expectedCount++; - assertEquals(VAL_UNFILTERED, entry.getValue()); + assertEquals(VAL_UNREDACTED, entry.getValue()); } else if (key.equals(KEY_NESTED)) { expectedCount++; Object value = entry.getValue(); - assertTrue(value instanceof FilteredMap); + assertTrue(value instanceof RedactedMap); } else if (key.equals(KEY_UNMODIFIABLE)) { expectedCount++; diff --git a/bugsnag/src/test/resources/logback.xml b/bugsnag/src/test/resources/logback.xml index 4450bab2..602c3576 100644 --- a/bugsnag/src/test/resources/logback.xml +++ b/bugsnag/src/test/resources/logback.xml @@ -7,8 +7,8 @@ gradleTask - password - credit_card_number + password + credit_card_number java.io.IOException com.example.Custom diff --git a/examples/logback/src/main/resources/logback.xml b/examples/logback/src/main/resources/logback.xml index 45b5ae69..86c109e4 100644 --- a/examples/logback/src/main/resources/logback.xml +++ b/examples/logback/src/main/resources/logback.xml @@ -17,8 +17,8 @@ - - + + diff --git a/features/filtering_metadata.feature b/features/filtering_metadata.feature deleted file mode 100644 index db8aacbf..00000000 --- a/features/filtering_metadata.feature +++ /dev/null @@ -1,54 +0,0 @@ -Feature: Metadata is filtered - -Scenario: Using the default metadata filter - When I run "AutoFilterScenario" 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 exception "message" equals "AutoFilterScenario" - And the event "metaData.custom.foo" equals "hunter2" - And the event "metaData.custom.password" equals "[FILTERED]" - And the event "metaData.user.password" equals "[FILTERED]" - -Scenario: Adding a custom metadata filter - When I run "ManualFilterScenario" 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 exception "message" equals "ManualFilterScenario" - And the event "metaData.custom.foo" equals "[FILTERED]" - And the event "metaData.user.foo" equals "[FILTERED]" - And the event "metaData.custom.bar" equals "hunter2" - -Scenario: Adding a thread metadata filter using logback - When I run "LogbackThreadMetaDataScenario" with logback config "meta_data_filter_config.xml" - 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 exception "message" equals "LogbackThreadMetaDataScenario" - And the event "metaData.thread.foo" equals "[FILTERED]" - And the event "metaData.thread.bar" equals "threadvalue2" - -Scenario: Adding a custom metadata filter using logback - When I run "LogbackMetaDataScenario" with logback config "meta_data_filter_config.xml" - 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 exception "message" equals "LogbackMetaDataScenario" - And the event "metaData.custom.foo" equals "[FILTERED]" - And the event "metaData.user.foo" equals "[FILTERED]" - And the event "metaData.custom.bar" equals "hunter2" - -Scenario: Using the default metadata filter in Spring Boot app - When I run spring boot "AutoFilterScenario" 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 exception "message" equals "AutoFilterScenario" - And the event "metaData.custom.foo" equals "hunter2" - And the event "metaData.custom.password" equals "[FILTERED]" - And the event "metaData.user.password" equals "[FILTERED]" - -Scenario: Using the default metadata filter in Spring app - When I run plain Spring "AutoFilterScenario" 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 exception "message" equals "AutoFilterScenario" - And the event "metaData.custom.foo" equals "hunter2" - And the event "metaData.custom.password" equals "[FILTERED]" - And the event "metaData.user.password" equals "[FILTERED]" diff --git a/features/fixtures/logback/meta_data_filter_config.xml b/features/fixtures/logback/meta_data_filter_config.xml deleted file mode 100644 index e036b689..00000000 --- a/features/fixtures/logback/meta_data_filter_config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - a35a2a72bd230ac0aa0f52715bbdc6aa - production - 1.0.0 - - foo - - http://localhost:9339/notify - - - - - - diff --git a/features/fixtures/mazerunner/src/main/resources/logback.xml b/features/fixtures/logback/meta_data_redact_config.xml similarity index 93% rename from features/fixtures/mazerunner/src/main/resources/logback.xml rename to features/fixtures/logback/meta_data_redact_config.xml index 60ade3a1..96009aaf 100644 --- a/features/fixtures/mazerunner/src/main/resources/logback.xml +++ b/features/fixtures/logback/meta_data_redact_config.xml @@ -6,9 +6,7 @@ a35a2a72bd230ac0aa0f52715bbdc6aa production 1.0.0 - - testAppType - + foo http://localhost:9339/notify @@ -16,3 +14,4 @@ + diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoFilterScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java similarity index 83% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoFilterScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java index 27b3de61..bfafab71 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoFilterScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java @@ -5,11 +5,11 @@ import com.bugsnag.callbacks.Callback; /** - * Sends a handled exception to Bugsnag, which contains metadata that should be filtered + * Sends a handled exception to Bugsnag, which contains metadata that should be redacted */ -public class AutoFilterScenario extends Scenario { +public class AutoRedactScenario extends Scenario { - public AutoFilterScenario(Bugsnag bugsnag) { + public AutoRedactScenario(Bugsnag bugsnag) { super(bugsnag); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualFilterScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java similarity index 78% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualFilterScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java index 7ae38999..22685510 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualFilterScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java @@ -5,18 +5,18 @@ import com.bugsnag.callbacks.Callback; /** - * Sends a handled exception to Bugsnag, which contains metadata that should be filtered + * Sends a handled exception to Bugsnag, which contains metadata that should be redacted */ -public class ManualFilterScenario extends Scenario { +public class ManualRedactScenario extends Scenario { - public ManualFilterScenario(Bugsnag bugsnag) { + public ManualRedactScenario(Bugsnag bugsnag) { super(bugsnag); } @Override public void run() { - bugsnag.setFilters("foo"); + bugsnag.setRedactedKeys("foo"); bugsnag.notify(generateException(), new Callback() { @Override diff --git a/features/redacting_metadata.feature b/features/redacting_metadata.feature new file mode 100644 index 00000000..e3d74a3d --- /dev/null +++ b/features/redacting_metadata.feature @@ -0,0 +1,55 @@ +Feature: Metadata is redacted + +Scenario: Using the default metadata redaction + When I run "AutoRedactScenario" 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 exception "message" equals "AutoRedactScenario" + And the event "metaData.custom.foo" equals "hunter2" + And the event "metaData.custom.password" equals "[REDACTED]" + And the event "metaData.user.password" equals "[REDACTED]" + +Scenario: Adding a custom metadata redaction + When I run "ManualRedactScenario" 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 exception "message" equals "ManualRedactScenario" + And the event "metaData.custom.foo" equals "[REDACTED]" + And the event "metaData.user.foo" equals "[REDACTED]" + And the event "metaData.custom.bar" equals "hunter2" + +Scenario: Adding a thread metadata redaction using logback + When I run "LogbackThreadMetaDataScenario" with logback config "meta_data_redact_config.xml" + 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 exception "message" equals "LogbackThreadMetaDataScenario" + And the event "metaData.thread.foo" equals "[REDACTED]" + And the event "metaData.thread.bar" equals "threadvalue2" + +Scenario: Adding a custom metadata redaction using logback + When I run "LogbackMetaDataScenario" with logback config "meta_data_redact_config.xml" + 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 exception "message" equals "LogbackMetaDataScenario" + And the event "metaData.custom.foo" equals "[REDACTED]" + And the event "metaData.user.foo" equals "[REDACTED]" + And the event "metaData.custom.bar" equals "hunter2" + +Scenario: Using the default metadata redaction in Spring Boot app + When I run spring boot "AutoRedactScenario" 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 exception "message" equals "AutoRedactScenario" + And the event "metaData.custom.foo" equals "hunter2" + And the event "metaData.custom.password" equals "[REDACTED]" + And the event "metaData.user.password" equals "[REDACTED]" + +Scenario: Using the default metadata redaction in Spring app + When I run plain Spring "AutoRedactScenario" 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 exception "message" equals "AutoRedactScenario" + And the event "metaData.custom.foo" equals "hunter2" + And the event "metaData.custom.password" equals "[REDACTED]" + And the event "metaData.user.password" equals "[REDACTED]" + diff --git a/features/scripts/assemble-fixtures.sh b/features/scripts/assemble-fixtures.sh index 1f53250c..27e63dc5 100755 --- a/features/scripts/assemble-fixtures.sh +++ b/features/scripts/assemble-fixtures.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash -if [ ! -d "features/fixtures/libs" ]; then - mkdir -p features/fixtures/libs - unzip maven-repository.zip -d features/fixtures/libs +# Remove existing directory if it exists +if [ -d "features/fixtures/libs" ]; then + rm -rf features/fixtures/libs fi + +# Recreate the directory +mkdir -p features/fixtures/libs + +# Unzip the contents into it +unzip maven-repository.zip -d features/fixtures/libs \ No newline at end of file From ddddf2c25ff52a1424a9ef55baf31d87cc115496 Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Mon, 3 Nov 2025 16:42:09 +0100 Subject: [PATCH 03/10] migrate groovy gradle files to kotlin gradle format (#238) --- Gemfile | 2 + bugsnag-spring/build.gradle | 53 -------- bugsnag-spring/build.gradle.kts | 50 +++++++ bugsnag/build.gradle | 59 --------- bugsnag/build.gradle.kts | 57 ++++++++ build.gradle => build.gradle.kts | 15 ++- common.gradle | 28 ---- common.gradle.kts | 16 +++ docker-compose.yml | 6 - dockerfiles/Dockerfile.java-common | 2 +- dockerfiles/Dockerfile.java-publisher | 2 +- dockerfiles/Dockerfile.java17-mazerunner | 2 +- dockerfiles/Dockerfile.java8-mazerunner | 20 --- dockerfiles/Dockerfile.license-audit | 2 +- features/fixtures/mazerunner/build.gradle | 41 ------ features/fixtures/mazerunner/build.gradle.kts | 36 +++++ .../{build.gradle => build.gradle.kts} | 21 +-- .../mazerunnerspringboot3/build.gradle | 6 +- .../mazerunnerspringboot3/build.gradle.kts | 37 ++++++ .../{build.gradle => build.gradle.kts} | 15 ++- features/fixtures/settings.gradle | 7 - features/fixtures/settings.gradle.kts | 5 + gradle/libs.versions.toml | 68 ++++++++++ release.gradle | 124 ----------------- release.gradle.kts | 125 ++++++++++++++++++ settings.gradle | 12 -- settings.gradle.kts | 10 ++ 27 files changed, 447 insertions(+), 374 deletions(-) delete mode 100644 bugsnag-spring/build.gradle create mode 100644 bugsnag-spring/build.gradle.kts delete mode 100644 bugsnag/build.gradle create mode 100644 bugsnag/build.gradle.kts rename build.gradle => build.gradle.kts (60%) delete mode 100644 common.gradle create mode 100644 common.gradle.kts delete mode 100644 dockerfiles/Dockerfile.java8-mazerunner delete mode 100644 features/fixtures/mazerunner/build.gradle create mode 100644 features/fixtures/mazerunner/build.gradle.kts rename features/fixtures/mazerunnerplainspring6/{build.gradle => build.gradle.kts} (66%) create mode 100644 features/fixtures/mazerunnerspringboot3/build.gradle.kts rename features/fixtures/scenarios/{build.gradle => build.gradle.kts} (57%) delete mode 100644 features/fixtures/settings.gradle create mode 100644 features/fixtures/settings.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 release.gradle create mode 100644 release.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/Gemfile b/Gemfile index 85813553..1dcbd4b1 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source 'https://rubygems.org' gem 'bugsnag-maze-runner', '~>9.0' gem 'os' +# Pin power_assert to avoid compatibility issues with test-unit +gem 'power_assert', '< 3.0.0' diff --git a/bugsnag-spring/build.gradle b/bugsnag-spring/build.gradle deleted file mode 100644 index 863e38b8..00000000 --- a/bugsnag-spring/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -ext { - jakartaSpringVersion = '6.0.0' - jakartaSpringBootVersion = '3.0.0' -} - -apply plugin: 'java-library' - -repositories { - mavenCentral() -} - -java { - withJavadocJar() - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } -} - - -tasks.register('sourceJar', Jar).configure { - from sourceSets.main.allJava -} - -// do not move this higher - sourceJar must be registered before we apply common.gradle -apply from: '../common.gradle' - -dependencies { - implementation project(':bugsnag') - - implementation "ch.qos.logback:logback-core:${logbackVersion}" - implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" - - implementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" - implementation "org.springframework:spring-webmvc:${jakartaSpringVersion}" - implementation "org.springframework.boot:spring-boot:${jakartaSpringBootVersion}" - implementation "org.springframework:spring-aop:${jakartaSpringVersion}" - - testImplementation project(':bugsnag').sourceSets.test.output - testImplementation project(':bugsnag') - testImplementation "junit:junit:${junitVersion}" - testImplementation "org.springframework.boot:spring-boot-starter-test:${jakartaSpringBootVersion}" - testImplementation "org.springframework.boot:spring-boot-starter-web:${jakartaSpringBootVersion}" - testImplementation "org.junit.jupiter:junit-jupiter:5.8.2" - testCompileOnly "org.mockito:mockito-core:2.10.0" -} - -/** ---- Publishing config ---- - * Pulls in publishing+signing rules from the shared release.gradle. - * This will create tasks like: - * :bugsnag:publishMavenJavaPublicationToTestRepository - * :bugsnag:publishMavenJavaPublicationToOssrhStagingRepository - */ -apply from: "${rootProject.projectDir}/release.gradle" \ No newline at end of file diff --git a/bugsnag-spring/build.gradle.kts b/bugsnag-spring/build.gradle.kts new file mode 100644 index 00000000..89b66137 --- /dev/null +++ b/bugsnag-spring/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + `java-library` +} + +repositories { + mavenCentral() +} + +java { + withJavadocJar() + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +val sourceJar by tasks.registering(Jar::class) { + from(sourceSets.main.get().allJava) +} + +// do not move this higher - sourceJar must be registered before we apply common.gradle.kts +apply(from = "../common.gradle.kts") + +dependencies { + implementation(project(":bugsnag")) + + implementation(libs.logback.core) + implementation(libs.slf4j.api) + + implementation(libs.jakarta.servlet.api) + implementation(libs.spring.webmvc) + implementation(libs.springBoot3.boot) + implementation(libs.spring.aop) + + testImplementation(project(":bugsnag").dependencyProject.sourceSets["test"].output) + testImplementation(project(":bugsnag")) + testImplementation(libs.junit) + testImplementation(libs.springBoot3.starter.test) + testImplementation(libs.springBoot3.starter.web) + testImplementation(libs.junit.jupiter) + testCompileOnly(libs.mockito.core.legacy) +} + +/** ---- Publishing config ---- + * Pulls in publishing+signing rules from the shared release.gradle.kts. + * This will create tasks like: + * :bugsnag:publishMavenJavaPublicationToTestRepository + * :bugsnag:publishMavenJavaPublicationToOssrhStagingRepository + */ +apply(from = "${rootProject.projectDir}/release.gradle.kts") + diff --git a/bugsnag/build.gradle b/bugsnag/build.gradle deleted file mode 100644 index 2d11d351..00000000 --- a/bugsnag/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - id "com.github.hierynomus.license" version "0.16.1" -} - -apply plugin: 'java-library' -apply from: '../common.gradle' - -compileJava { - sourceCompatibility = '17' - targetCompatibility = '17' -} - -compileTestJava { - sourceCompatibility = '17' - targetCompatibility = '17' -} - -repositories { - mavenCentral() -} - -dependencies { - api "com.fasterxml.jackson.core:jackson-databind:2.14.1" - api "org.slf4j:slf4j-api:${slf4jApiVersion}" - compileOnly "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" - compileOnly("ch.qos.logback:logback-classic:${logbackVersion}") { - exclude group: "org.slf4j" - } - - testImplementation "junit:junit:${junitVersion}" - testImplementation "org.slf4j:log4j-over-slf4j:${slf4jApiVersion}" - testImplementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation("ch.qos.logback:logback-classic:${logbackVersion}") { - exclude group: "org.slf4j" - } -} - -// license checking -license { - header rootProject.file('LICENSE') - ignoreFailures true -} - -downloadLicenses { - dependencyConfiguration "compile" -} - -java { - withJavadocJar() -} - -/** ---- Publishing config ---- - * Pulls in publishing+signing rules from the shared release.gradle. - * This will create tasks like: - * :bugsnag:publishMavenJavaPublicationToTestRepository - * :bugsnag:publishMavenJavaPublicationToOssrhStagingRepository - */ -apply from: "${rootProject.projectDir}/release.gradle" \ No newline at end of file diff --git a/bugsnag/build.gradle.kts b/bugsnag/build.gradle.kts new file mode 100644 index 00000000..8b0845c4 --- /dev/null +++ b/bugsnag/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.license) + `java-library` +} + +apply(from = "../common.gradle.kts") + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +repositories { + mavenCentral() +} + +dependencies { + api(libs.jackson.databind) + api(libs.slf4j.api) + compileOnly(libs.jakarta.servlet.api) + compileOnly(libs.logback.classic) { + exclude(group = "org.slf4j") + } + + testImplementation(libs.junit) + testImplementation(libs.log4j.over.slf4j) + testImplementation(libs.jakarta.servlet.api) + testImplementation(libs.mockito.core) + testImplementation(libs.logback.classic) { + exclude(group = "org.slf4j") + } +} + +// license checking +configure { + header = rootProject.file("LICENSE") + isIgnoreFailures = true +} + +tasks.named("downloadLicenses") { + // Note: dependencyConfiguration property needs to be set through the plugin's DSL + // This may require checking the plugin documentation for Kotlin DSL syntax +} + +java { + withJavadocJar() +} + +/** ---- Publishing config ---- + * Pulls in publishing+signing rules from the shared release.gradle.kts. + * This will create tasks like: + * :bugsnag:publishMavenJavaPublicationToTestRepository + * :bugsnag:publishMavenJavaPublicationToOssrhStagingRepository + */ +apply(from = "${rootProject.projectDir}/release.gradle.kts") + diff --git a/build.gradle b/build.gradle.kts similarity index 60% rename from build.gradle rename to build.gradle.kts index b0895907..67a917c9 100644 --- a/build.gradle +++ b/build.gradle.kts @@ -8,14 +8,15 @@ buildscript { } gradle.projectsEvaluated { - tasks.withType(JavaCompile) { + tasks.withType().configureEach { // fail build on warnings, disable options complaining about Java 6 compatibility when building with JDK 7+ - options.compilerArgs << "-Xlint:all" << "-Werror" << "-Xlint:-options" + options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror", "-Xlint:-options")) } } -tasks.register('publishToTestRepoAll') { - dependsOn subprojects.collect { - tasks.findByPath("${it.path}:publishAllPublicationsToTestRepository") ?: [] - }.flatten() -} \ No newline at end of file +tasks.register("publishToTestRepoAll") { + dependsOn(subprojects.mapNotNull { + tasks.findByPath("${it.path}:publishAllPublicationsToTestRepository") + }) +} + diff --git a/common.gradle b/common.gradle deleted file mode 100644 index 4c803b16..00000000 --- a/common.gradle +++ /dev/null @@ -1,28 +0,0 @@ -ext { - jakartaServletApiVersion = "5.0.0" - logbackVersion = "1.2.3" - slf4jApiVersion = "1.7.25" - junitVersion = "4.13.2" - mockitoVersion = "5.0.0" -} - -if (JavaVersion.current().isJava8Compatible()) { - apply plugin: 'checkstyle' -} - -if (project.hasProperty('releasing') && project.depth <= 1) { - apply from: "../release.gradle" -} - -test { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - } -} - -if (JavaVersion.current().isJava8Compatible()) { - checkstyle { - toolVersion = "8.18" - configFile = new File(rootDir, "config/checkstyle/checkstyle.xml") - } -} \ No newline at end of file diff --git a/common.gradle.kts b/common.gradle.kts new file mode 100644 index 00000000..d02d54dc --- /dev/null +++ b/common.gradle.kts @@ -0,0 +1,16 @@ +// Version catalog is now used - see gradle/libs.versions.toml +apply(plugin = "checkstyle") + +if (project.hasProperty("releasing") && project.depth <= 1) { + apply(from = "../release.gradle.kts") +} +tasks.named("test") { + testLogging { + events("passed", "skipped", "failed", "standardOut", "standardError") + } +} + +configure { + toolVersion = "8.18" + configFile = file("${rootDir}/config/checkstyle/checkstyle.xml") +} diff --git a/docker-compose.yml b/docker-compose.yml index 242c3de5..03711a89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,12 +9,6 @@ services: dockerfile: dockerfiles/Dockerfile.java-common volumes: - .:/app - java8-mazerunner: - build: - context: . - dockerfile: dockerfiles/Dockerfile.java8-mazerunner - volumes: - - .:/app java17-mazerunner: build: context: . diff --git a/dockerfiles/Dockerfile.java-common b/dockerfiles/Dockerfile.java-common index 2d4d1224..ff0f7cb5 100644 --- a/dockerfiles/Dockerfile.java-common +++ b/dockerfiles/Dockerfile.java-common @@ -9,5 +9,5 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y zip COPY gradlew gradle.properties /app/ COPY gradle/ /app/gradle/ ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" -COPY settings.gradle /app/ +COPY settings.gradle.kts /app/ RUN ./gradlew diff --git a/dockerfiles/Dockerfile.java-publisher b/dockerfiles/Dockerfile.java-publisher index c213d08e..904e3b89 100644 --- a/dockerfiles/Dockerfile.java-publisher +++ b/dockerfiles/Dockerfile.java-publisher @@ -5,7 +5,7 @@ WORKDIR /app # Copy gradle files COPY gradlew gradle.properties /app/ COPY gradle/ /app/gradle/ -COPY build.gradle settings.gradle release.gradle common.gradle /app/ +COPY build.gradle.kts settings.gradle.kts release.gradle.kts common.gradle.kts /app/ # Copy sdk source files COPY bugsnag/ bugsnag/ diff --git a/dockerfiles/Dockerfile.java17-mazerunner b/dockerfiles/Dockerfile.java17-mazerunner index f4fef7ff..222c17d4 100644 --- a/dockerfiles/Dockerfile.java17-mazerunner +++ b/dockerfiles/Dockerfile.java17-mazerunner @@ -9,7 +9,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive \ COPY gradlew gradle.properties /app/ COPY gradle/ /app/gradle/ ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" -COPY settings.gradle /app/ +COPY settings.gradle.kts /app/ RUN ./gradlew # Copy repo into docker diff --git a/dockerfiles/Dockerfile.java8-mazerunner b/dockerfiles/Dockerfile.java8-mazerunner deleted file mode 100644 index 123f0e30..00000000 --- a/dockerfiles/Dockerfile.java8-mazerunner +++ /dev/null @@ -1,20 +0,0 @@ -FROM tomcat:9.0.56-jdk8 -WORKDIR /app - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive \ - apt-get install -y -q docker-compose bundler libcurl4-openssl-dev unzip - -# Force download of gradle zip early to avoid repeating -# if Docker cache is invalidated by branch changes. -COPY gradlew gradle.properties /app/ -COPY gradle/ /app/gradle/ -ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" -COPY settings.gradle /app/ -RUN ./gradlew - -# Copy repo into docker -COPY . /app - -# Setup mazerunner -RUN gem install bundler:1.16.5 -RUN bundle install diff --git a/dockerfiles/Dockerfile.license-audit b/dockerfiles/Dockerfile.license-audit index c94c939f..d6c4109e 100644 --- a/dockerfiles/Dockerfile.license-audit +++ b/dockerfiles/Dockerfile.license-audit @@ -9,7 +9,7 @@ WORKDIR /scan COPY gradle gradle COPY bugsnag bugsnag COPY bugsnag-spring bugsnag-spring -COPY build.gradle common.gradle gradle.properties gradlew gradlew.bat LICENSE release.gradle settings.gradle ./ +COPY build.gradle.kts common.gradle.kts gradle.properties gradlew gradlew.bat LICENSE release.gradle.kts settings.gradle.kts ./ RUN ./gradlew diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle deleted file mode 100644 index bd500ac6..00000000 --- a/features/fixtures/mazerunner/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -buildscript { - ext { - springBootVersion = '3.5.6' - } - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") - } -} - -apply plugin: 'java' -apply plugin: 'org.springframework.boot' -apply plugin: 'io.spring.dependency-management' - -group 'com.bugsnag.mazerunner' -version '1.0-SNAPSHOT' - -sourceCompatibility = '17' - -repositories { - mavenCentral() - maven { - url file('../libs').toURI() - } -} - -dependencies { - implementation("org.springframework.boot:spring-boot-starter") - implementation("ch.qos.logback:logback-classic:1.5.18") - implementation("ch.qos.logback:logback-core:1.5.18") - implementation("org.codehaus.janino:janino:3.1.10") - - implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.1") - implementation("com.fasterxml.jackson.core:jackson-databind:2.14.1") - implementation("com.bugsnag:bugsnag:9.9.9-test") - implementation project(":scenarios") - - testImplementation group: 'junit', name: 'junit', version: '4.13.2' -} diff --git a/features/fixtures/mazerunner/build.gradle.kts b/features/fixtures/mazerunner/build.gradle.kts new file mode 100644 index 00000000..bb343ac0 --- /dev/null +++ b/features/fixtures/mazerunner/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + java + id("org.springframework.boot") version "3.5.6" + id("io.spring.dependency-management") version "1.1.0" +} + +group = "com.bugsnag.mazerunner" +version = "1.0-SNAPSHOT" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +repositories { + mavenCentral() + maven { + url = file("../libs").toURI() + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("ch.qos.logback:logback-classic:1.5.18") + implementation("ch.qos.logback:logback-core:1.5.18") + implementation("org.codehaus.janino:janino:3.1.10") + + implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.1") + implementation("com.bugsnag:bugsnag:9.9.9-test") + implementation(project(":scenarios")) + + testImplementation("junit:junit:4.13.2") +} + diff --git a/features/fixtures/mazerunnerplainspring6/build.gradle b/features/fixtures/mazerunnerplainspring6/build.gradle.kts similarity index 66% rename from features/fixtures/mazerunnerplainspring6/build.gradle rename to features/fixtures/mazerunnerplainspring6/build.gradle.kts index ab40ef70..9b5bdefc 100644 --- a/features/fixtures/mazerunnerplainspring6/build.gradle +++ b/features/fixtures/mazerunnerplainspring6/build.gradle.kts @@ -1,15 +1,19 @@ plugins { - id "war" + war } -group 'com.bugsnag.mazerunnerplainspring' +group = "com.bugsnag.mazerunnerplainspring" -sourceCompatibility = '17' +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} repositories { mavenCentral() maven { - url file('../libs').toURI() + url = file("../libs").toURI() } } @@ -23,9 +27,10 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.14.1") implementation("com.bugsnag:bugsnag:9.9.9-test") implementation("com.bugsnag:bugsnag-spring:9.9.9-test") - implementation project(":scenarios") + implementation(project(":scenarios")) +} + +tasks.named("war") { + archiveFileName.set("mazerunnerplainspring.war") } -war { - archiveFileName.set('mazerunnerplainspring.war') -} \ No newline at end of file diff --git a/features/fixtures/mazerunnerspringboot3/build.gradle b/features/fixtures/mazerunnerspringboot3/build.gradle index 2d7863ed..4a10e93b 100644 --- a/features/fixtures/mazerunnerspringboot3/build.gradle +++ b/features/fixtures/mazerunnerspringboot3/build.gradle @@ -17,7 +17,11 @@ apply plugin: 'io.spring.dependency-management' group 'com.bugsnag.mazerunnerspringboot3' version '1.0-SNAPSHOT' -sourceCompatibility = '17' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} repositories { mavenCentral() diff --git a/features/fixtures/mazerunnerspringboot3/build.gradle.kts b/features/fixtures/mazerunnerspringboot3/build.gradle.kts new file mode 100644 index 00000000..f63817d9 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + java + id("org.springframework.boot") version "3.5.6" + id("io.spring.dependency-management") version "1.1.0" +} + +group = "com.bugsnag.mazerunnerspringboot3" +version = "1.0-SNAPSHOT" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +repositories { + mavenCentral() + maven { + url = file("../libs").toURI() + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("ch.qos.logback:logback-classic:1.5.18") + implementation("org.codehaus.janino:janino:3.1.10") + + implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.1") + implementation("com.bugsnag:bugsnag:9.9.9-test") + implementation("com.bugsnag:bugsnag-spring:9.9.9-test") + implementation(project(":scenarios")) + + testImplementation("junit:junit:4.13.2") +} + diff --git a/features/fixtures/scenarios/build.gradle b/features/fixtures/scenarios/build.gradle.kts similarity index 57% rename from features/fixtures/scenarios/build.gradle rename to features/fixtures/scenarios/build.gradle.kts index 276da29f..8057ca9d 100644 --- a/features/fixtures/scenarios/build.gradle +++ b/features/fixtures/scenarios/build.gradle.kts @@ -1,13 +1,19 @@ -apply plugin: 'java' +plugins { + java +} -group 'com.bugsnag.mazerunner' +group = "com.bugsnag.mazerunner" -sourceCompatibility = '17' +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} repositories { mavenCentral() maven { - url file('../libs').toURI() + url = file("../libs").toURI() } } @@ -16,3 +22,4 @@ dependencies { implementation("org.codehaus.janino:janino:3.1.10") implementation("com.bugsnag:bugsnag:9.9.9-test") } + diff --git a/features/fixtures/settings.gradle b/features/fixtures/settings.gradle deleted file mode 100644 index 6a101a85..00000000 --- a/features/fixtures/settings.gradle +++ /dev/null @@ -1,7 +0,0 @@ -include ':mazerunner', - ':scenarios' - -if (JavaVersion.current() >= JavaVersion.VERSION_17) { - include ':mazerunnerspringboot3', - ':mazerunnerplainspring6' -} diff --git a/features/fixtures/settings.gradle.kts b/features/fixtures/settings.gradle.kts new file mode 100644 index 00000000..31c0edab --- /dev/null +++ b/features/fixtures/settings.gradle.kts @@ -0,0 +1,5 @@ +include(":mazerunner") +include(":scenarios") +include(":mazerunnerspringboot3") +include(":mazerunnerplainspring6") + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..23a4ffe1 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,68 @@ +[versions] +# Build plugins +gradleLicense = "0.16.1" +checkstyle = "8.18" +gretty = "4.0.3" + +# Java/Kotlin toolchain +java = "17" + +# Main dependencies +jackson = "2.14.1" +slf4j = "1.7.25" +logback = "1.2.3" +jakartaServlet = "5.0.0" + +# Spring +spring = "6.0.0" +springBoot2 = "2.5.14" +springBoot3 = "3.0.0" + +# Testing +junit = "4.13.2" +junitJupiter = "5.8.2" +mockito = "5.0.0" +mockitoLegacy = "2.10.0" + +[libraries] +# Jackson +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } + +# SLF4J +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } +log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "slf4j" } + +# Logback +logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } + +# Jakarta +jakarta-servlet-api = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakartaServlet" } + +# Spring Framework +spring-webmvc = { module = "org.springframework:spring-webmvc", version.ref = "spring" } +spring-aop = { module = "org.springframework:spring-aop", version.ref = "spring" } + +# Spring Boot 2.x +springBoot2-gradle-plugin = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "springBoot2" } +springBoot2-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot2" } +springBoot2-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springBoot2" } + +# Spring Boot 3.x +springBoot3-boot = { module = "org.springframework.boot:spring-boot", version.ref = "springBoot3" } +springBoot3-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "springBoot3" } +springBoot3-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springBoot3" } + +# Testing +junit = { module = "junit:junit", version.ref = "junit" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junitJupiter" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-core-legacy = { module = "org.mockito:mockito-core", version.ref = "mockitoLegacy" } + +# Build tools +gretty = { module = "org.gretty:gretty", version.ref = "gretty" } + +[plugins] +license = { id = "com.github.hierynomus.license", version.ref = "gradleLicense" } + diff --git a/release.gradle b/release.gradle deleted file mode 100644 index 975476fc..00000000 --- a/release.gradle +++ /dev/null @@ -1,124 +0,0 @@ -// release.gradle — script plugin (apply from: ...) - -apply plugin: 'java' -apply plugin: 'maven-publish' -apply plugin: 'signing' - -/** -------- Helpers -------- */ -def projectProperty = { String k, def d = null -> project.findProperty(k) ?: d } // gradle property (no envs) -def projectBoolean = { String k -> (projectProperty(k)?.toString()?.toBoolean() ?: false) } - -def isRelease = projectBoolean('releasing') // set with -Preleasing=true on real deploys - -// Prefer Gradle properties (works on CI without env vars). -// Put these in ~/.gradle/gradle.properties or project gradle.properties on CI. -def ossrhUser = projectProperty('NEXUS_USERNAME') ?: projectProperty('ossrhUsername') -def ossrhPass = projectProperty('NEXUS_PASSWORD') ?: projectProperty('ossrhPassword') - -// Optional in-memory signing keys (recommended for CI) -// signingKey: ASCII-armored private key, signingPassword: key passphrase -def signingKey = projectProperty('signingKey') -def signingPassword = projectProperty('signingPassword') - -/** -------- Java / Artifacts -------- */ -base { - archivesName = projectProperty('artifactId') ?: project.name -} - -java { - toolchain { languageVersion = JavaLanguageVersion.of(17) } - withSourcesJar() - withJavadocJar() -} - -/** -------- Publishing -------- */ -publishing { - repositories { - // Always-available local test repo - maven { - name = 'test' - url = uri(rootProject.file('build/repository')) - } - - // OSSRH only if credentials are available (no envs needed) - if (ossrhUser && ossrhPass) { - maven { - name = 'ossrhStaging' - url = uri('https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/') - credentials { - username = ossrhUser - password = ossrhPass - } - } - } else { - logger.lifecycle("[publishing] OSSRH credentials not provided; skipping remote repository configuration.") - } - } - - publications { - mavenJava(MavenPublication) { - from components.java - - groupId = 'com.bugsnag' - artifactId = projectProperty('artifactId') ?: project.name - version = rootProject.version - - pom { - name = projectProperty('projectName') ?: artifactId - description = projectProperty('projectDescription') ?: "Bugsnag Java Notifier" - url = 'https://github.com/bugsnag/bugsnag-java' - - scm { - url = 'https://github.com/bugsnag/bugsnag-java' - connection = 'scm:git:git://github.com/bugsnag/bugsnag-java.git' - developerConnection = 'scm:git:ssh://git@github.com/bugsnag/bugsnag-java.git' - } - - licenses { - license { - name = 'MIT' - url = 'http://opensource.org/licenses/MIT' - distribution = 'repo' - } - } - - organization { - name = 'Bugsnag' - url = 'https://bugsnag.com' - } - - developers { - developer { - id = 'loopj' - name = 'James Smith' - email = 'james@bugsnag.com' - } - } - } - } - } -} - -/** -------- Signing (no .asc on test publishes) -------- */ -signing { - // Only *require* signing for real releases - required { isRelease } - - // Only attach signatures when we actually want to sign - if (isRelease) { - if (signingKey && signingPassword) { - useInMemoryPgpKeys(signingKey as String, signingPassword as String) - } - sign(publishing.publications['mavenJava']) - } -} - -/** -------- Safety: never try publishing to OSSRH without creds -------- */ -import org.gradle.api.publish.maven.tasks.PublishToMavenRepository - -tasks.withType(PublishToMavenRepository).configureEach { - onlyIf { - // Allow all "test" publishes, and only allow OSSRH when creds exist - repository?.name != 'ossrhStaging' || (ossrhUser && ossrhPass) - } -} \ No newline at end of file diff --git a/release.gradle.kts b/release.gradle.kts new file mode 100644 index 00000000..bb62cb2b --- /dev/null +++ b/release.gradle.kts @@ -0,0 +1,125 @@ +// release.gradle.kts — script plugin (apply from: ...) + +apply(plugin = "java") +apply(plugin = "maven-publish") +apply(plugin = "signing") + +/** -------- Helpers -------- */ +fun projectProperty(k: String): String? = project.findProperty(k) as String? +fun projectBoolean(k: String): Boolean = projectProperty(k)?.toBoolean() ?: false + +val isRelease = projectBoolean("releasing") // set with -Preleasing=true on real deploys + +// Prefer Gradle properties (works on CI without env vars). +// Put these in ~/.gradle/gradle.properties or project gradle.properties on CI. +val ossrhUser = projectProperty("NEXUS_USERNAME") ?: projectProperty("ossrhUsername") +val ossrhPass = projectProperty("NEXUS_PASSWORD") ?: projectProperty("ossrhPassword") + +// Optional in-memory signing keys (recommended for CI) +// signingKey: ASCII-armored private key, signingPassword: key passphrase +val signingKey = projectProperty("signingKey") +val signingPassword = projectProperty("signingPassword") + +/** -------- Java / Artifacts -------- */ +configure { + archivesName.set(projectProperty("artifactId") ?: project.name) +} + +configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + withSourcesJar() + withJavadocJar() +} + +/** -------- Publishing -------- */ +configure { + repositories { + // Always-available local test repo + maven { + name = "test" + url = uri(rootProject.file("build/repository")) + } + + // OSSRH only if credentials are available (no envs needed) + if (ossrhUser != null && ossrhPass != null) { + maven { + name = "ossrhStaging" + url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/") + credentials { + username = ossrhUser + password = ossrhPass + } + } + } else { + logger.lifecycle("[publishing] OSSRH credentials not provided; skipping remote repository configuration.") + } + } + + publications { + create("mavenJava") { + from(components["java"]) + + groupId = "com.bugsnag" + artifactId = projectProperty("artifactId") ?: project.name + version = rootProject.version.toString() + + pom { + name.set(projectProperty("projectName") ?: artifactId) + description.set(projectProperty("projectDescription") ?: "Bugsnag Java Notifier") + url.set("https://github.com/bugsnag/bugsnag-java") + + scm { + url.set("https://github.com/bugsnag/bugsnag-java") + connection.set("scm:git:git://github.com/bugsnag/bugsnag-java.git") + developerConnection.set("scm:git:ssh://git@github.com/bugsnag/bugsnag-java.git") + } + + licenses { + license { + name.set("MIT") + url.set("http://opensource.org/licenses/MIT") + distribution.set("repo") + } + } + + organization { + name.set("Bugsnag") + url.set("https://bugsnag.com") + } + + developers { + developer { + id.set("loopj") + name.set("James Smith") + email.set("james@bugsnag.com") + } + } + } + } + } +} + +/** -------- Signing (no .asc on test publishes) -------- */ +configure { + // Only *require* signing for real releases + setRequired { isRelease } + + // Only attach signatures when we actually want to sign + if (isRelease) { + if (signingKey != null && signingPassword != null) { + useInMemoryPgpKeys(signingKey, signingPassword) + } + sign(the().publications["mavenJava"]) + } +} + +/** -------- Safety: never try publishing to OSSRH without creds -------- */ +tasks.withType().configureEach { + onlyIf { + // Allow all "test" publishes, and only allow OSSRH when creds exist + repository?.name != "ossrhStaging" || (ossrhUser != null && ossrhPass != null) + } +} + diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 9c8a3d8f..00000000 --- a/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -include ':bugsnag', - ':bugsnag-spring' - -include ':examples:simple', - ':examples:spring', - ':examples:spring-web', - ':examples:logback' - -// jakarta servlet example requires java 11 compatibility for gretty plugin -if (JavaVersion.current().isJava11Compatible()) { - include ':examples:servlet-jakarta' -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..caa631b3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ + +include(":bugsnag") +include(":bugsnag-spring") + +include(":examples:simple") +include(":examples:spring") +include(":examples:spring-web") +include(":examples:logback") +include(":examples:servlet-jakarta") + From 9c140d860a6b5ae41e90731b08faa57d256b4770 Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Mon, 10 Nov 2025 16:54:38 +0100 Subject: [PATCH 04/10] rename MetaData to Metadata (#237) --- .buildkite/pipeline.yml | 2 + .../test/java/com/bugsnag/SpringMvcTest.java | 2 +- .../src/main/java/com/bugsnag/Bugsnag.java | 24 ++-- .../java/com/bugsnag/BugsnagAppender.java | 25 ++-- .../main/java/com/bugsnag/Diagnostics.java | 2 +- .../bugsnag/{MetaData.java => Metadata.java} | 8 +- bugsnag/src/main/java/com/bugsnag/Report.java | 15 ++- .../callbacks/JakartaServletCallback.java | 2 +- .../com/bugsnag/logback/LogbackMetaData.java | 24 ---- .../com/bugsnag/logback/LogbackMetadata.java | 24 ++++ ...taDataKey.java => LogbackMetadataKey.java} | 4 +- ...taDataTab.java => LogbackMetadataTab.java} | 10 +- .../BugsnagServletRequestListener.java | 2 +- ...ataTest.java => AppenderMetadataTest.java} | 78 +++++------ .../test/java/com/bugsnag/AppenderTest.java | 20 +-- .../test/java/com/bugsnag/BugsnagTest.java | 6 +- .../bugsnag/JakartaServletCallbackTest.java | 2 +- .../test/java/com/bugsnag/MetaDataTest.java | 72 ---------- .../test/java/com/bugsnag/MetadataTest.java | 72 ++++++++++ ...aDataTest.java => ThreadMetadataTest.java} | 124 +++++++++--------- bugsnag/src/test/resources/logback.xml | 4 +- docker-compose.yml | 2 + dockerfiles/Dockerfile.java-common | 2 +- dockerfiles/Dockerfile.java17-mazerunner | 3 +- dockerfiles/Dockerfile.license-audit | 2 +- .../example/logback/cli/Application.java | 12 +- .../logback/src/main/resources/logback.xml | 10 +- .../bugsnag/example/simple/ExampleApp.java | 2 +- .../spring/web/ApplicationRestController.java | 4 +- .../bugsnag/example/spring/web/Config.java | 2 +- .../cli/ApplicationCommandLineRunner.java | 4 +- .../fixtures/logback/meta_data_config.xml | 4 +- .../AsyncMethodService.java | 8 +- .../ScheduledTaskService.java | 8 +- .../AsyncMethodService.java | 10 +- .../ScheduledTaskService.java | 8 +- .../TestRestController.java | 2 +- ...ario.java => LogbackMetadataScenario.java} | 8 +- ...ava => LogbackThreadMetadataScenario.java} | 14 +- ...ataScenario.java => MetadataScenario.java} | 4 +- ...nario.java => ThreadMetadataScenario.java} | 12 +- ...a => UnhandledThreadMetadataScenario.java} | 14 +- features/meta_data.feature | 54 ++++---- features/redacting_metadata.feature | 8 +- 44 files changed, 366 insertions(+), 353 deletions(-) rename bugsnag/src/main/java/com/bugsnag/{MetaData.java => Metadata.java} (80%) delete mode 100644 bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaData.java create mode 100644 bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadata.java rename bugsnag/src/main/java/com/bugsnag/logback/{LogbackMetaDataKey.java => LogbackMetadataKey.java} (84%) rename bugsnag/src/main/java/com/bugsnag/logback/{LogbackMetaDataTab.java => LogbackMetadataTab.java} (65%) rename bugsnag/src/test/java/com/bugsnag/{AppenderMetaDataTest.java => AppenderMetadataTest.java} (67%) delete mode 100644 bugsnag/src/test/java/com/bugsnag/MetaDataTest.java create mode 100644 bugsnag/src/test/java/com/bugsnag/MetadataTest.java rename bugsnag/src/test/java/com/bugsnag/{ThreadMetaDataTest.java => ThreadMetadataTest.java} (59%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{LogbackMetaDataScenario.java => LogbackMetadataScenario.java} (79%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{LogbackThreadMetaDataScenario.java => LogbackThreadMetadataScenario.java} (62%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{MetaDataScenario.java => MetadataScenario.java} (84%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{ThreadMetaDataScenario.java => ThreadMetadataScenario.java} (79%) rename features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/{UnhandledThreadMetaDataScenario.java => UnhandledThreadMetadataScenario.java} (74%) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 2842b995..36a73f20 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -31,6 +31,7 @@ steps: run: java17-mazerunner - artifacts#v1.9.0: download: "maven-repository.zip" + upload: "maze_output/maze_output.zip" command: - 'features/scripts/assemble-fixtures.sh' - 'bundle exec maze-runner --exclude=features/[^a-m].*.feature' @@ -44,6 +45,7 @@ steps: run: java17-mazerunner - artifacts#v1.9.0: download: "maven-repository.zip" + upload: "maze_output/maze_output.zip" command: - 'features/scripts/assemble-fixtures.sh' - 'bundle exec maze-runner --exclude=features/[^n-z].*.feature' diff --git a/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java index ed03f19f..6acb31a1 100644 --- a/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java +++ b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java @@ -136,7 +136,7 @@ public void requestMetadataSetCorrectly() { // Check that the request metadata is set as expected @SuppressWarnings(value = "unchecked") Map requestMetadata = - (Map) report.getMetaData().get("request"); + (Map) report.getMetadata().get("request"); assertEquals("http://localhost:" + randomServerPort + "/throw-runtime-exception", requestMetadata.get("url")); assertEquals("GET", requestMetadata.get("method")); diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 697e433d..82ba35a0 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -58,10 +58,10 @@ public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) { private Configuration config; private final SessionTracker sessionTracker; - private static final ThreadLocal THREAD_METADATA = new ThreadLocal() { + private static final ThreadLocal THREAD_METADATA = new ThreadLocal() { @Override - public MetaData initialValue() { - return new MetaData(); + public Metadata initialValue() { + return new Metadata(); } }; @@ -228,13 +228,13 @@ public void setEndpoint(String endpoint) { } /** - * Set which keys should be redacted when sending metaData to Bugsnag. + * Set which keys should be redacted when sending metadata to Bugsnag. * Use this when you want to ensure sensitive information, such as passwords - * or credit card information is stripped from metaData you send to Bugsnag. - * Any keys in metaData which contain these strings will be marked as + * or credit card information is stripped from metadata you send to Bugsnag. + * Any keys in metadata which contain these strings will be marked as * [REDACTED] when send to Bugsnag. * - * @param redactedKeys a list of String keys to redact from metaData + * @param redactedKeys a list of String keys to redact from metadata */ public void setRedactedKeys(String... redactedKeys) { config.redactedKeys = redactedKeys; @@ -465,7 +465,7 @@ public boolean notify(Report report, Callback reportCallback) { } // Add thread metadata to the report - report.mergeMetaData(THREAD_METADATA.get()); + report.mergeMetadata(THREAD_METADATA.get()); // Run the report-specific beforeNotify callback, if given if (reportCallback != null) { @@ -618,14 +618,14 @@ public void close() { * @param key the key of the metadata to add * @param value the metadata value to add */ - public static void addThreadMetaData(String tabName, String key, Object value) { + public static void addThreadMetadata(String tabName, String key, Object value) { THREAD_METADATA.get().addToTab(tabName, key, value); } /** * Clears all metadata added to the current thread */ - public static void clearThreadMetaData() { + public static void clearThreadMetadata() { THREAD_METADATA.get().clear(); } @@ -634,7 +634,7 @@ public static void clearThreadMetaData() { * * @param tabName the name of the tab to remove */ - public static void clearThreadMetaData(String tabName) { + public static void clearThreadMetadata(String tabName) { THREAD_METADATA.get().clearTab(tabName); } @@ -644,7 +644,7 @@ public static void clearThreadMetaData(String tabName) { * @param tabName the name of the tab to that the metadata is in * @param key the key of the metadata to remove */ - public static void clearThreadMetaData(String tabName, String key) { + public static void clearThreadMetadata(String tabName, String key) { THREAD_METADATA.get().clearKey(tabName, key); } diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 74f890ca..cc99c42e 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -3,9 +3,9 @@ import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.logback.BugsnagMarker; -import com.bugsnag.logback.LogbackMetaData; -import com.bugsnag.logback.LogbackMetaDataKey; -import com.bugsnag.logback.LogbackMetaDataTab; +import com.bugsnag.logback.LogbackMetadata; +import com.bugsnag.logback.LogbackMetadataKey; +import com.bugsnag.logback.LogbackMetadataTab; import com.bugsnag.logback.ProxyConfiguration; import ch.qos.logback.classic.Level; @@ -75,7 +75,7 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase { /** Application version. */ private String appVersion; - private List globalMetaData = new ArrayList(); + private List globalMetadata = new ArrayList(); /** Bugsnag client. */ private Bugsnag bugsnag = null; @@ -272,9 +272,9 @@ private Bugsnag createBugsnag() { @Override public void beforeNotify(Report report) { - for (LogbackMetaData metaData : globalMetaData) { - for (LogbackMetaDataTab tab : metaData.getTabs()) { - for (LogbackMetaDataKey key : tab.getKeys()) { + for (LogbackMetadata metadata : globalMetadata) { + for (LogbackMetadataTab tab : metadata.getTabs()) { + for (LogbackMetadataKey key : tab.getKeys()) { report.addToTab(tab.getName(), key.getName(), key.getValue()); @@ -524,10 +524,15 @@ public void setAppVersion(String appVersion) { * Internal use only * Should only be used via the logback.xml file * - * @param metaData Adds meta data to every report + * @param metadata Adds metadata to every report */ - public void setMetaData(LogbackMetaData metaData) { - this.globalMetaData.add(metaData); + public void setMetadata(LogbackMetadata metadata) { + this.globalMetadata.add(metadata); + } + + @Deprecated + public void setMetaData(LogbackMetadata metadata) { + setMetadata(metadata); } /** diff --git a/bugsnag/src/main/java/com/bugsnag/Diagnostics.java b/bugsnag/src/main/java/com/bugsnag/Diagnostics.java index d6aa3e1a..671e3fed 100644 --- a/bugsnag/src/main/java/com/bugsnag/Diagnostics.java +++ b/bugsnag/src/main/java/com/bugsnag/Diagnostics.java @@ -11,7 +11,7 @@ class Diagnostics { Map app; Map device; Map user = new HashMap(); - MetaData metaData = new MetaData(); + Metadata metadata = new Metadata(); Diagnostics(Configuration configuration) { app = getDefaultAppInfo(configuration); diff --git a/bugsnag/src/main/java/com/bugsnag/MetaData.java b/bugsnag/src/main/java/com/bugsnag/Metadata.java similarity index 80% rename from bugsnag/src/main/java/com/bugsnag/MetaData.java rename to bugsnag/src/main/java/com/bugsnag/Metadata.java index 33d215fd..92fd4ea6 100644 --- a/bugsnag/src/main/java/com/bugsnag/MetaData.java +++ b/bugsnag/src/main/java/com/bugsnag/Metadata.java @@ -3,7 +3,7 @@ import java.util.HashMap; import java.util.Map; -class MetaData extends HashMap { +class Metadata extends HashMap { private static final long serialVersionUID = 2530038179702722770L; public void addToTab(String tabName, String key, Object value) { @@ -20,9 +20,9 @@ void clearKey(String tabName, String key) { tab.remove(key); } - void merge(MetaData metaData) { - for (String tabName : metaData.keySet()) { - getTab(tabName).putAll(metaData.getTab(tabName)); + void merge(Metadata metadata) { + for (String tabName : metadata.keySet()) { + getTab(tabName).putAll(metadata.getTab(tabName)); } } diff --git a/bugsnag/src/main/java/com/bugsnag/Report.java b/bugsnag/src/main/java/com/bugsnag/Report.java index baba0569..5f963baf 100644 --- a/bugsnag/src/main/java/com/bugsnag/Report.java +++ b/bugsnag/src/main/java/com/bugsnag/Report.java @@ -2,6 +2,8 @@ import com.bugsnag.serialization.Expose; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -123,8 +125,9 @@ public Map getUser() { } @Expose - public Map getMetaData() { - return new RedactedMap(diagnostics.metaData, Set.of(config.redactedKeys)); + @JsonProperty("metaData") + public Map getMetadata() { + return new RedactedMap(diagnostics.metadata, Set.of(config.redactedKeys)); } @Expose @@ -186,7 +189,7 @@ public String getExceptionMessage() { * @return the modified report */ public Report addToTab(String tabName, String key, Object value) { - diagnostics.metaData.addToTab(tabName, key, value); + diagnostics.metadata.addToTab(tabName, key, value); return this; } @@ -197,7 +200,7 @@ public Report addToTab(String tabName, String key, Object value) { * @return The message from the exception contained in this error report. */ public Report clearTab(String tabName) { - diagnostics.metaData.clearTab(tabName); + diagnostics.metadata.clearTab(tabName); return this; } @@ -331,8 +334,8 @@ void setHandledState(HandledState handledState) { this.handledState = handledState; } - void mergeMetaData(MetaData metaData) { - diagnostics.metaData.merge(metaData); + void mergeMetadata(Metadata metadata) { + diagnostics.metadata.merge(metadata); } static class SeverityReason { diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java index 3945f48e..17c4f67c 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java @@ -33,7 +33,7 @@ public void beforeNotify(Report report) { return; } - // Add request information to metaData + // Add request information to metadata report .addToTab("request", "url", request.getRequestURL().toString()) .addToTab("request", "method", request.getMethod()) diff --git a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaData.java b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaData.java deleted file mode 100644 index d600811a..00000000 --- a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaData.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.bugsnag.logback; - -import java.util.ArrayList; -import java.util.List; - -/** Used to allow meta data to be added in the logback.xml file */ -public class LogbackMetaData { - - private List tabs = new ArrayList(); - - /** - * @return The tabs in the meta data - */ - public List getTabs() { - return tabs; - } - - /** - * @param tab a new tab to add to the meta data - */ - public void setTab(LogbackMetaDataTab tab) { - this.tabs.add(tab); - } -} diff --git a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadata.java b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadata.java new file mode 100644 index 00000000..b02394a3 --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadata.java @@ -0,0 +1,24 @@ +package com.bugsnag.logback; + +import java.util.ArrayList; +import java.util.List; + +/** Used to allow metadata to be added in the logback.xml file */ +public class LogbackMetadata { + + private List tabs = new ArrayList(); + + /** + * @return The tabs in the metadata + */ + public List getTabs() { + return tabs; + } + + /** + * @param tab a new tab to add to the metadata + */ + public void setTab(LogbackMetadataTab tab) { + this.tabs.add(tab); + } +} diff --git a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaDataKey.java b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadataKey.java similarity index 84% rename from bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaDataKey.java rename to bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadataKey.java index 77732c45..eee1301a 100644 --- a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaDataKey.java +++ b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadataKey.java @@ -1,7 +1,7 @@ package com.bugsnag.logback; -/** Used to allow meta data to be added in the logback.xml file */ -public class LogbackMetaDataKey { +/** Used to allow metadata to be added in the logback.xml file */ +public class LogbackMetadataKey { private String name; diff --git a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaDataTab.java b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadataTab.java similarity index 65% rename from bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaDataTab.java rename to bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadataTab.java index 048b240b..24954caf 100644 --- a/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetaDataTab.java +++ b/bugsnag/src/main/java/com/bugsnag/logback/LogbackMetadataTab.java @@ -3,12 +3,12 @@ import java.util.ArrayList; import java.util.List; -/** Used to allow meta data to be added in the logback.xml file */ -public class LogbackMetaDataTab { +/** Used to allow metadata to be added in the logback.xml file */ +public class LogbackMetadataTab { private String name; - private List keys = new ArrayList(); + private List keys = new ArrayList(); /** * @return The name of the tab @@ -27,14 +27,14 @@ public void setName(String name) { /** * @return The keys in the tab */ - public List getKeys() { + public List getKeys() { return keys; } /** * @param key A key to add to the tab */ - public void setKey(LogbackMetaDataKey key) { + public void setKey(LogbackMetadataKey key) { this.keys.add(key); } } diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java b/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java index 1867078f..3455bc54 100644 --- a/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java +++ b/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java @@ -29,7 +29,7 @@ public void requestInitialized(ServletRequestEvent servletRequestEvent) { @Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) { SERVLET_REQUEST.remove(); - Bugsnag.clearThreadMetaData(); + Bugsnag.clearThreadMetadata(); } private void trackServletSession() { diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderMetaDataTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java similarity index 67% rename from bugsnag/src/test/java/com/bugsnag/AppenderMetaDataTest.java rename to bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java index c756ee1f..4ba4dc0a 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderMetaDataTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java @@ -21,11 +21,11 @@ import java.util.Map; /** - * Test for using meta data via the Bugsnag Appender + * Test for using metadata via the Bugsnag Appender */ -public class AppenderMetaDataTest { +public class AppenderMetadataTest { - private static final Logger LOGGER = LoggerFactory.getLogger(AppenderMetaDataTest.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppenderMetadataTest.class); private StubNotificationDelivery delivery; private Delivery originalDelivery; private BugsnagAppender appender; @@ -56,15 +56,15 @@ public void revertDelivery() { } @Test - public void testMetaDataFromLogbackFile() { + public void testMetadataFromLogbackFile() { // Send a log message LOGGER.warn("Test exception", new RuntimeException("test")); // Get the notification details Notification notification = delivery.getNotifications().get(0); - assertTrue(notification.getEvents().get(0).getMetaData().containsKey("logbackTab")); - Map myTab = getMetaDataMap(notification, "logbackTab"); + assertTrue(notification.getEvents().get(0).getMetadata().containsKey("logbackTab")); + Map myTab = getMetadataMap(notification, "logbackTab"); assertEquals("logbackValue1", myTab.get("logbackKey1")); assertEquals("logbackValue2", myTab.get("logbackKey2")); @@ -72,27 +72,27 @@ public void testMetaDataFromLogbackFile() { @Test @SuppressWarnings (value = "unchecked") - public void testMetaDataTypes() { + public void testMetadataTypes() { - Bugsnag.addThreadMetaData("myTab", "string key", "string value"); - Bugsnag.addThreadMetaData("myTab", "bool key", true); - Bugsnag.addThreadMetaData("myTab", "int key", 1); - Bugsnag.addThreadMetaData("myTab", "float key", 1.1); + Bugsnag.addThreadMetadata("myTab", "string key", "string value"); + Bugsnag.addThreadMetadata("myTab", "bool key", true); + Bugsnag.addThreadMetadata("myTab", "int key", 1); + Bugsnag.addThreadMetadata("myTab", "float key", 1.1); Map map = new HashMap(); map.put("key", "value"); - Bugsnag.addThreadMetaData("myTab", "object key", map); + Bugsnag.addThreadMetadata("myTab", "object key", map); Integer[] array = new Integer[] {1, 2, 3, 4, 5}; - Bugsnag.addThreadMetaData("myTab", "array key", array); + Bugsnag.addThreadMetadata("myTab", "array key", array); // Send a log message LOGGER.warn("Test exception", new RuntimeException("test")); // Get the notification details Notification notification = delivery.getNotifications().get(0); - assertTrue(notification.getEvents().get(0).getMetaData().containsKey("myTab")); - Map myTab = getMetaDataMap(notification, "myTab"); + assertTrue(notification.getEvents().get(0).getMetadata().containsKey("myTab")); + Map myTab = getMetadataMap(notification, "myTab"); assertEquals("string value", myTab.get("string key")); assertEquals(true, myTab.get("bool key")); @@ -103,12 +103,12 @@ public void testMetaDataTypes() { } @Test - public void testMetaDataRemoval() { + public void testMetadataRemoval() { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "some key", "some thread value"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "some key", "some thread value"); - // Send three test logs, the first one with report meta data added + // Send three test logs, the first one with report metadata added LOGGER.warn(new BugsnagMarker(new Callback() { @Override public void beforeNotify(Report report) { @@ -118,39 +118,39 @@ public void beforeNotify(Report report) { LOGGER.warn("Test exception", new RuntimeException("test")); - Bugsnag.clearThreadMetaData(); + Bugsnag.clearThreadMetadata(); LOGGER.warn("Test exception", new RuntimeException("test")); // Check that three reports were sent to Bugsnag assertEquals(3, delivery.getNotifications().size()); - // Check the meta data is set as expected - // Should have both report and thread meta data + // Check the metadata is set as expected + // Should have both report and thread metadata Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("report")); - assertTrue(report.getMetaData().containsKey("thread")); - assertEquals("some report value", getMetaDataMap(notification, "report").get("some key")); - assertEquals("some thread value", getMetaDataMap(notification, "thread").get("some key")); + assertTrue(report.getMetadata().containsKey("report")); + assertTrue(report.getMetadata().containsKey("thread")); + assertEquals("some report value", getMetadataMap(notification, "report").get("some key")); + assertEquals("some thread value", getMetadataMap(notification, "thread").get("some key")); - // Should have just thread meta data + // Should have just thread metadata notification = delivery.getNotifications().get(1); report = notification.getEvents().get(0); - assertFalse(report.getMetaData().containsKey("report")); - assertTrue(report.getMetaData().containsKey("thread")); - assertEquals("some thread value", getMetaDataMap(notification, "thread").get("some key")); + assertFalse(report.getMetadata().containsKey("report")); + assertTrue(report.getMetadata().containsKey("thread")); + assertEquals("some thread value", getMetadataMap(notification, "thread").get("some key")); - // Should have neither meta data + // Should have neither metadata notification = delivery.getNotifications().get(2); report = notification.getEvents().get(0); - assertFalse(report.getMetaData().containsKey("report")); - assertFalse(report.getMetaData().containsKey("thread")); + assertFalse(report.getMetadata().containsKey("report")); + assertFalse(report.getMetadata().containsKey("thread")); } @Test @SuppressWarnings (value = "unchecked") - public void testMetaDataFromMdc() { + public void testMetadataFromMdc() { MDC.put("context key1", "context value1"); MDC.put("context key2", "context value2"); @@ -160,22 +160,22 @@ public void testMetaDataFromMdc() { // Get the notification details Notification notification = delivery.getNotifications().get(0); - assertTrue(notification.getEvents().get(0).getMetaData().containsKey("Context")); - Map myTab = getMetaDataMap(notification, "Context"); + assertTrue(notification.getEvents().get(0).getMetadata().containsKey("Context")); + Map myTab = getMetadataMap(notification, "Context"); assertEquals("context value1", myTab.get("context key1")); assertEquals("context value2", myTab.get("context key2")); } /** - * Gets a hashmap key from the meta data in a notification + * Gets a hashmap key from the metadata in a notification * * @param notification The notification * @param key The key to get * @return The hash map */ @SuppressWarnings (value = "unchecked") - private Map getMetaDataMap(Notification notification, String key) { - return ((Map) notification.getEvents().get(0).getMetaData().get(key)); + private Map getMetadataMap(Notification notification, String key) { + return ((Map) notification.getEvents().get(0).getMetadata().get(key)); } } diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index 0d7a56f0..34c65ef0 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -87,7 +87,7 @@ public void testSimpleException() { assertEquals("test", notification.getEvents().get(0).getExceptionMessage()); assertEquals(Severity.WARNING.getValue(), notification.getEvents().get(0).getSeverity()); assertEquals("Test exception", - getMetaDataMap(notification, "Log event data").get("Message")); + getMetadataMap(notification, "Log event data").get("Message")); } @Test @@ -109,7 +109,7 @@ public void testFormattedMessage() { assertEquals("test", notification.getEvents().get(0).getExceptionMessage()); assertEquals(Severity.WARNING.getValue(), notification.getEvents().get(0).getSeverity()); assertEquals("Test exception, errorCode: " + value, - getMetaDataMap(notification, "Log event data").get("Message")); + getMetadataMap(notification, "Log event data").get("Message")); } @Test @@ -277,19 +277,19 @@ public void testAppType() { public void testRedactedKeys() { // Add some metadata which should be redacted by key name - Bugsnag.addThreadMetaData("myTab", "password", "password value"); - Bugsnag.addThreadMetaData("myTab", "credit_card_number", "card number"); - Bugsnag.addThreadMetaData("myTab", "mysecret", "not redacted"); + Bugsnag.addThreadMetadata("myTab", "password", "password value"); + Bugsnag.addThreadMetadata("myTab", "credit_card_number", "card number"); + Bugsnag.addThreadMetadata("myTab", "mysecret", "not redacted"); // Send a log message - LOGGER.warn("Exception with redacted meta data", new RuntimeException("test")); + LOGGER.warn("Exception with redacted metadata", new RuntimeException("test")); // Check that a report was sent to Bugsnag assertEquals(1, delivery.getNotifications().size()); Notification notification = delivery.getNotifications().get(0); - assertTrue(notification.getEvents().get(0).getMetaData().containsKey("myTab")); - Map myTab = getMetaDataMap(notification, "myTab"); + assertTrue(notification.getEvents().get(0).getMetadata().containsKey("myTab")); + Map myTab = getMetadataMap(notification, "myTab"); assertEquals("[REDACTED]", myTab.get("password")); assertEquals("[REDACTED]", myTab.get("credit_card_number")); @@ -419,7 +419,7 @@ private SessionTracker getSessionTracker(Bugsnag bugsnag) { * @return The hash map */ @SuppressWarnings (value = "unchecked") - private Map getMetaDataMap(Notification notification, String key) { - return ((Map) notification.getEvents().get(0).getMetaData().get(key)); + private Map getMetadataMap(Notification notification, String key) { + return ((Map) notification.getEvents().get(0).getMetadata().get(key)); } } diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index 0884fc72..4b9443d9 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -172,9 +172,9 @@ public void testRedactedKeys() { public void deliver(Serializer serializer, Object object, Map headers) { Report report = ((Notification) object).getEvents().get(0); Map firstTab = - (Map) report.getMetaData().get("firsttab"); + (Map) report.getMetadata().get("firsttab"); final Map secondTab = - (Map) report.getMetaData().get("secondtab"); + (Map) report.getMetadata().get("secondtab"); assertEquals("[REDACTED]", firstTab.get("testredact1")); assertEquals("[REDACTED]", firstTab.get("testredact2")); assertEquals("secretpassword", firstTab.get("testredact3")); @@ -204,7 +204,7 @@ public void testRedactHeaders() { public void deliver(Serializer serializer, Object object, Map headers) { Report report = ((Notification) object).getEvents().get(0); Map requestTab = - (Map) report.getMetaData().get("request"); + (Map) report.getMetadata().get("request"); Map headersMap = (Map) requestTab.get("headers"); diff --git a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java index f5e6677e..00ad7997 100644 --- a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java +++ b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java @@ -85,7 +85,7 @@ public void testRequestMetadataAdded() { JakartaServletCallback callback = new JakartaServletCallback(); callback.beforeNotify(report); - Map metadata = report.getMetaData(); + Map metadata = report.getMetadata(); assertTrue(metadata.containsKey("request")); Map request = (Map) metadata.get("request"); diff --git a/bugsnag/src/test/java/com/bugsnag/MetaDataTest.java b/bugsnag/src/test/java/com/bugsnag/MetaDataTest.java deleted file mode 100644 index d2f4b580..00000000 --- a/bugsnag/src/test/java/com/bugsnag/MetaDataTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.bugsnag; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import org.junit.Test; - -import java.util.Map; - -public class MetaDataTest { - - @Test - public void testEmptyMetaData() { - MetaData metaData = new MetaData(); - assertEquals(0, metaData.size()); - } - - @Test - @SuppressWarnings("unchecked") - public void testSingleTabSingleValue() { - MetaData metaData = new MetaData(); - metaData.addToTab("tab-name", "key-1", "value-1"); - - assertEquals(1, metaData.size()); - assertEquals(1, ((Map) metaData.get("tab-name")).size()); - assertEquals("value-1", ((Map) metaData.get("tab-name")).get("key-1")); - } - - @Test - @SuppressWarnings("unchecked") - public void testSingleTabMultipleValues() { - MetaData metaData = new MetaData(); - metaData.addToTab("tab-name", "key-1", "value-1"); - metaData.addToTab("tab-name", "key-2", "value-2"); - - assertEquals(1, metaData.size()); - assertEquals(2, ((Map) metaData.get("tab-name")).size()); - assertEquals("value-1", ((Map) metaData.get("tab-name")).get("key-1")); - assertEquals("value-2", ((Map) metaData.get("tab-name")).get("key-2")); - } - - @Test - @SuppressWarnings("unchecked") - public void testMultipleTabs() { - MetaData metaData = new MetaData(); - metaData.addToTab("tab-name-1", "key-1", "value-1"); - metaData.addToTab("tab-name-2", "key-1", "value-1"); - - assertEquals(2, metaData.size()); - assertEquals(1, ((Map) metaData.get("tab-name-1")).size()); - assertEquals(1, ((Map) metaData.get("tab-name-2")).size()); - assertEquals("value-1", ((Map) metaData.get("tab-name-1")).get("key-1")); - assertEquals("value-1", ((Map) metaData.get("tab-name-1")).get("key-1")); - } - - @Test - @SuppressWarnings("unchecked") - public void testClearTab() { - MetaData metaData = new MetaData(); - metaData.addToTab("tab-name-1", "key-1", "value-1"); - metaData.addToTab("tab-name-2", "key-1", "value-1"); - - assertEquals(2, metaData.size()); - - metaData.clearTab("tab-name-1"); - - assertEquals(1, metaData.size()); - - assertNull(metaData.get("tab-name-1")); - assertEquals(1, ((Map) metaData.get("tab-name-2")).size()); - } -} diff --git a/bugsnag/src/test/java/com/bugsnag/MetadataTest.java b/bugsnag/src/test/java/com/bugsnag/MetadataTest.java new file mode 100644 index 00000000..a28c4975 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/MetadataTest.java @@ -0,0 +1,72 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import java.util.Map; + +public class MetadataTest { + + @Test + public void testEmptyMetadata() { + Metadata metadata = new Metadata(); + assertEquals(0, metadata.size()); + } + + @Test + @SuppressWarnings("unchecked") + public void testSingleTabSingleValue() { + Metadata metadata = new Metadata(); + metadata.addToTab("tab-name", "key-1", "value-1"); + + assertEquals(1, metadata.size()); + assertEquals(1, ((Map) metadata.get("tab-name")).size()); + assertEquals("value-1", ((Map) metadata.get("tab-name")).get("key-1")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSingleTabMultipleValues() { + Metadata metadata = new Metadata(); + metadata.addToTab("tab-name", "key-1", "value-1"); + metadata.addToTab("tab-name", "key-2", "value-2"); + + assertEquals(1, metadata.size()); + assertEquals(2, ((Map) metadata.get("tab-name")).size()); + assertEquals("value-1", ((Map) metadata.get("tab-name")).get("key-1")); + assertEquals("value-2", ((Map) metadata.get("tab-name")).get("key-2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testMultipleTabs() { + Metadata metadata = new Metadata(); + metadata.addToTab("tab-name-1", "key-1", "value-1"); + metadata.addToTab("tab-name-2", "key-1", "value-1"); + + assertEquals(2, metadata.size()); + assertEquals(1, ((Map) metadata.get("tab-name-1")).size()); + assertEquals(1, ((Map) metadata.get("tab-name-2")).size()); + assertEquals("value-1", ((Map) metadata.get("tab-name-1")).get("key-1")); + assertEquals("value-1", ((Map) metadata.get("tab-name-1")).get("key-1")); + } + + @Test + @SuppressWarnings("unchecked") + public void testClearTab() { + Metadata metadata = new Metadata(); + metadata.addToTab("tab-name-1", "key-1", "value-1"); + metadata.addToTab("tab-name-2", "key-1", "value-1"); + + assertEquals(2, metadata.size()); + + metadata.clearTab("tab-name-1"); + + assertEquals(1, metadata.size()); + + assertNull(metadata.get("tab-name-1")); + assertEquals(1, ((Map) metadata.get("tab-name-2")).size()); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/ThreadMetaDataTest.java b/bugsnag/src/test/java/com/bugsnag/ThreadMetadataTest.java similarity index 59% rename from bugsnag/src/test/java/com/bugsnag/ThreadMetaDataTest.java rename to bugsnag/src/test/java/com/bugsnag/ThreadMetadataTest.java index 26589564..2aa1fe07 100644 --- a/bugsnag/src/test/java/com/bugsnag/ThreadMetaDataTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ThreadMetadataTest.java @@ -12,7 +12,7 @@ import java.util.Map; -public class ThreadMetaDataTest { +public class ThreadMetadataTest { private StubNotificationDelivery delivery; private Delivery originalDelivery; @@ -39,39 +39,39 @@ public void revertDelivery() { } @Test - public void testMetaDataClearAll() { + public void testMetadataClearAll() { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "some key", "some thread value"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "some key", "some thread value"); bugsnag.notify(new RuntimeException("test")); - Bugsnag.clearThreadMetaData(); + Bugsnag.clearThreadMetadata(); bugsnag.notify(new RuntimeException("test")); // Check that two reports were sent to Bugsnag assertEquals(2, delivery.getNotifications().size()); - // Check the meta data is added to the first report + // Check the metadata is added to the first report Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("thread")); - assertEquals("some thread value", getMetaDataMap(notification, "thread").get("some key")); + assertTrue(report.getMetadata().containsKey("thread")); + assertEquals("some thread value", getMetadataMap(notification, "thread").get("some key")); - // Check the meta data is not added to the second report + // Check the metadata is not added to the second report notification = delivery.getNotifications().get(1); report = notification.getEvents().get(0); - assertFalse(report.getMetaData().containsKey("thread")); + assertFalse(report.getMetadata().containsKey("thread")); } @Test - public void testMetaDataClearTab() { + public void testMetadataClearTab() { - // Add some thread meta data - Bugsnag.addThreadMetaData("tab1", "some key", "some value"); - Bugsnag.addThreadMetaData("tab2", "some key", "some value"); + // Add some thread metadata + Bugsnag.addThreadMetadata("tab1", "some key", "some value"); + Bugsnag.addThreadMetadata("tab2", "some key", "some value"); bugsnag.notify(new RuntimeException("test")); - Bugsnag.clearThreadMetaData("tab2"); + Bugsnag.clearThreadMetadata("tab2"); bugsnag.notify(new RuntimeException("test")); // Check that two reports were sent to Bugsnag @@ -80,28 +80,28 @@ public void testMetaDataClearTab() { // Check that both tabs are populated in the first report Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("tab1")); - assertEquals("some value", getMetaDataMap(notification, "tab1").get("some key")); - assertTrue(report.getMetaData().containsKey("tab2")); - assertEquals("some value", getMetaDataMap(notification, "tab2").get("some key")); + assertTrue(report.getMetadata().containsKey("tab1")); + assertEquals("some value", getMetadataMap(notification, "tab1").get("some key")); + assertTrue(report.getMetadata().containsKey("tab2")); + assertEquals("some value", getMetadataMap(notification, "tab2").get("some key")); // Check that only the first tab is in the second tab notification = delivery.getNotifications().get(1); report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("tab1")); - assertEquals("some value", getMetaDataMap(notification, "tab1").get("some key")); - assertFalse(report.getMetaData().containsKey("tab2")); + assertTrue(report.getMetadata().containsKey("tab1")); + assertEquals("some value", getMetadataMap(notification, "tab1").get("some key")); + assertFalse(report.getMetadata().containsKey("tab2")); } @Test - public void testMetaDataClearKey() { + public void testMetadataClearKey() { - // Add some thread meta data - Bugsnag.addThreadMetaData("tab1", "key1", "some value"); - Bugsnag.addThreadMetaData("tab1", "key2", "some value"); + // Add some thread metadata + Bugsnag.addThreadMetadata("tab1", "key1", "some value"); + Bugsnag.addThreadMetadata("tab1", "key2", "some value"); bugsnag.notify(new RuntimeException("test")); - Bugsnag.clearThreadMetaData("tab1", "key2"); + Bugsnag.clearThreadMetadata("tab1", "key2"); bugsnag.notify(new RuntimeException("test")); // Check that two reports were sent to Bugsnag @@ -110,29 +110,29 @@ public void testMetaDataClearKey() { // Check that both keys are populated in the first report Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("tab1")); - assertEquals("some value", getMetaDataMap(notification, "tab1").get("key1")); - assertEquals("some value", getMetaDataMap(notification, "tab1").get("key2")); + assertTrue(report.getMetadata().containsKey("tab1")); + assertEquals("some value", getMetadataMap(notification, "tab1").get("key1")); + assertEquals("some value", getMetadataMap(notification, "tab1").get("key2")); // Check that only the first tab is in the second tab notification = delivery.getNotifications().get(1); report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("tab1")); - assertEquals("some value", getMetaDataMap(notification, "tab1").get("key1")); - assertFalse(getMetaDataMap(notification, "tab1").containsKey("key2")); + assertTrue(report.getMetadata().containsKey("tab1")); + assertEquals("some value", getMetadataMap(notification, "tab1").get("key1")); + assertFalse(getMetadataMap(notification, "tab1").containsKey("key2")); } @Test - public void testInnerThreadMetaData() { + public void testInnerThreadMetadata() { - // Add some thread meta data in the outer thread - Bugsnag.addThreadMetaData("outerthread", "some key", "value should not be in report"); + // Add some thread metadata in the outer thread + Bugsnag.addThreadMetadata("outerthread", "some key", "value should not be in report"); Thread thread = new Thread(new Runnable() { @Override public void run() { - // Add thread meta data which should get associated with the exception - Bugsnag.addThreadMetaData("innerthread", "some key", "value should be in report"); + // Add thread metadata which should get associated with the exception + Bugsnag.addThreadMetadata("innerthread", "some key", "value should be in report"); // Notify to Bugsnag bugsnag.notify(new RuntimeException("test")); @@ -151,26 +151,26 @@ public void run() { Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("innerthread")); + assertTrue(report.getMetadata().containsKey("innerthread")); assertEquals("value should be in report", - getMetaDataMap(notification, "innerthread").get("some key")); + getMetadataMap(notification, "innerthread").get("some key")); - assertFalse(report.getMetaData().containsKey("outerthread")); + assertFalse(report.getMetadata().containsKey("outerthread")); } @Test - public void testUnhandledThreadMetaDataRemoval() { + public void testUnhandledThreadMetadataRemoval() { Thread thread = new Thread(new Runnable() { @Override public void run() { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "key1", "should be cleared from metadata"); - Bugsnag.clearThreadMetaData(); + Bugsnag.clearThreadMetadata(); - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "key2", "should be included in metadata"); // Thrown unhandled exception throw new RuntimeException("test"); @@ -189,23 +189,23 @@ public void run() { Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("thread")); - assertFalse(getMetaDataMap(notification, "thread").containsKey("key1")); - assertEquals("should be included in meta data", - getMetaDataMap(notification, "thread").get("key2")); + assertTrue(report.getMetadata().containsKey("thread")); + assertFalse(getMetadataMap(notification, "thread").containsKey("key1")); + assertEquals("should be included in metadata", + getMetadataMap(notification, "thread").get("key2")); } @Test - public void testUnhandledThreadMetaData() { + public void testUnhandledThreadMetadata() { - // Add some thread meta data in the outer thread - Bugsnag.addThreadMetaData("outerthread", "some key", "value should not be in report"); + // Add some thread metadata in the outer thread + Bugsnag.addThreadMetadata("outerthread", "some key", "value should not be in report"); Thread thread = new Thread(new Runnable() { @Override public void run() { - // Add thread meta data which should get associated with the exception - Bugsnag.addThreadMetaData("innerthread", "some key", "value should be in report"); + // Add thread metadata which should get associated with the exception + Bugsnag.addThreadMetadata("innerthread", "some key", "value should be in report"); // Thrown unhandled exception throw new RuntimeException("test"); @@ -224,22 +224,22 @@ public void run() { Notification notification = delivery.getNotifications().get(0); Report report = notification.getEvents().get(0); - assertTrue(report.getMetaData().containsKey("innerthread")); + assertTrue(report.getMetadata().containsKey("innerthread")); assertEquals("value should be in report", - getMetaDataMap(notification, "innerthread").get("some key")); + getMetadataMap(notification, "innerthread").get("some key")); - assertFalse(report.getMetaData().containsKey("outerthread")); + assertFalse(report.getMetadata().containsKey("outerthread")); } /** - * Gets a hashmap key from the meta data in a notification + * Gets a hashmap key from the metadata in a notification * * @param notification The notification * @param key The key to get * @return The hash map */ @SuppressWarnings (value = "unchecked") - private Map getMetaDataMap(Notification notification, String key) { - return ((Map) notification.getEvents().get(0).getMetaData().get(key)); + private Map getMetadataMap(Notification notification, String key) { + return ((Map) notification.getEvents().get(0).getMetadata().get(key)); } } diff --git a/bugsnag/src/test/resources/logback.xml b/bugsnag/src/test/resources/logback.xml index 602c3576..40270aa5 100644 --- a/bugsnag/src/test/resources/logback.xml +++ b/bugsnag/src/test/resources/logback.xml @@ -19,7 +19,7 @@ com.company.package1 com.company.package2 - + logbackTab @@ -31,7 +31,7 @@ logbackValue2 - + true diff --git a/docker-compose.yml b/docker-compose.yml index 03711a89..78db1435 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: build: context: . dockerfile: dockerfiles/Dockerfile.java17-mazerunner + volumes: + - ./maze_output:/app/maze_output java-publisher: build: context: . diff --git a/dockerfiles/Dockerfile.java-common b/dockerfiles/Dockerfile.java-common index ff0f7cb5..e3ce526f 100644 --- a/dockerfiles/Dockerfile.java-common +++ b/dockerfiles/Dockerfile.java-common @@ -1,4 +1,4 @@ -FROM openjdk:17-jdk-slim +FROM eclipse-temurin:17.0.13_11-jdk-jammy WORKDIR /app RUN apt-get update > /dev/null diff --git a/dockerfiles/Dockerfile.java17-mazerunner b/dockerfiles/Dockerfile.java17-mazerunner index 222c17d4..8ca1b00d 100644 --- a/dockerfiles/Dockerfile.java17-mazerunner +++ b/dockerfiles/Dockerfile.java17-mazerunner @@ -1,4 +1,5 @@ -FROM tomcat:10-jdk17-openjdk-slim +FROM tomcat:10.1-jdk17-temurin-jammy + WORKDIR /app RUN apt-get update && DEBIAN_FRONTEND=noninteractive \ diff --git a/dockerfiles/Dockerfile.license-audit b/dockerfiles/Dockerfile.license-audit index d6c4109e..ed96ff1b 100644 --- a/dockerfiles/Dockerfile.license-audit +++ b/dockerfiles/Dockerfile.license-audit @@ -1,4 +1,4 @@ -FROM openjdk:17-jdk-slim +FROM eclipse-temurin:17.0.13_11-jdk-jammy RUN apt-get update RUN apt-get install -y ruby-full curl diff --git a/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java b/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java index fc38b4bd..08b77577 100644 --- a/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java +++ b/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java @@ -31,8 +31,8 @@ public static void main(String[] args) throws Exception { }); } - // Add meta data that will be added to all reports on the current thread - Bugsnag.addThreadMetaData("thread tab", "thread key 1", "thread value 1"); + // Add metadata that will be added to all reports on the current thread + Bugsnag.addThreadMetadata("thread tab", "thread key 1", "thread value 1"); // Send a handled exception to Bugsnag LOGGER.info("Sending a handled exception to Bugsnag"); @@ -50,8 +50,8 @@ public static void main(String[] args) throws Exception { LOGGER.info(e.getMessage(), e); } - // Send a handled exception with custom MetaData - LOGGER.info("Sending a handled exception to Bugsnag with custom MetaData"); + // Send a handled exception with custom Metadata + LOGGER.info("Sending a handled exception to Bugsnag with custom Metadata"); try { throw new RuntimeException("Handled exception - custom metadata"); } catch (RuntimeException e) { @@ -76,8 +76,8 @@ public void run() { // Wait for unhandled exception thread to finish before exiting thread.join(); - // Remove the thread meta data so it won't be added to future reports on this thread - Bugsnag.clearThreadMetaData(); + // Remove the thread metadata so it won't be added to future reports on this thread + Bugsnag.clearThreadMetadata(); // Exit the application System.exit(0); diff --git a/examples/logback/src/main/resources/logback.xml b/examples/logback/src/main/resources/logback.xml index 86c109e4..a62e51a0 100644 --- a/examples/logback/src/main/resources/logback.xml +++ b/examples/logback/src/main/resources/logback.xml @@ -29,15 +29,15 @@ - + - + - - + + - + diff --git a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java index ec6ba056..cfb24441 100644 --- a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java +++ b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java @@ -47,7 +47,7 @@ public void beforeNotify(Report report) { bugsnag.notify(e, Severity.INFO); } - // Send a handled exception with custom MetaData + // Send a handled exception with custom Metadata try { throw new RuntimeException("Handled exception - custom metadata"); } catch (RuntimeException e) { diff --git a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java index b5d23736..df5a5796 100644 --- a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java +++ b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java @@ -65,7 +65,7 @@ public String sendHandledExceptionInfo() { @RequestMapping("/send-handled-exception-with-metadata") public String sendHandledExceptionWithMetadata() { - LOGGER.info("Sending a handled exception to Bugsnag with custom MetaData"); + LOGGER.info("Sending a handled exception to Bugsnag with custom Metadata"); try { throw new RuntimeException("Handled exception - custom metadata"); } catch (RuntimeException e) { @@ -79,7 +79,7 @@ public void beforeNotify(Report report) { }); } - return exampleWebsiteLinks + "
Sent a handled exception to Bugsnag with custom MetaData"; + return exampleWebsiteLinks + "
Sent a handled exception to Bugsnag with custom Metadata"; } @RequestMapping("/send-unhandled-exception") diff --git a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java index e5ba4493..0481f59f 100644 --- a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java +++ b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java @@ -50,7 +50,7 @@ public void beforeNotify(Report report) { public String exampleWebsiteLinks() { return "Send a handled exception to Bugsnag
" + "Send a handled exception to Bugsnag with INFO severity
" - + "Send a handled exception to Bugsnag with custom MetaData
" + + "Send a handled exception to Bugsnag with custom Metadata
" + "Send an unhandled exception to Bugsnag
" + "Send an unhandled exception to Bugsnag from an async method
" + "Send an unhandled exception to Bugsnag from an async method that returns a Future
" diff --git a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java index 7d6cdd7c..6e201859 100644 --- a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java +++ b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java @@ -43,8 +43,8 @@ public void run(final String... args) throws Exception { bugsnag.notify(e, Severity.INFO); } - // Send a handled exception with custom MetaData - LOGGER.info("Sending a handled exception to Bugsnag with custom MetaData"); + // Send a handled exception with custom Metadata + LOGGER.info("Sending a handled exception to Bugsnag with custom Metadata"); try { throw new RuntimeException("Handled exception - custom metadata"); } catch (RuntimeException e) { diff --git a/features/fixtures/logback/meta_data_config.xml b/features/fixtures/logback/meta_data_config.xml index 48704afb..38376a96 100644 --- a/features/fixtures/logback/meta_data_config.xml +++ b/features/fixtures/logback/meta_data_config.xml @@ -7,7 +7,7 @@ production 1.0.0 - + configTab @@ -19,7 +19,7 @@ tabValue2 - + http://localhost:9339/notify diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java index f1d52269..20c58073 100644 --- a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java @@ -13,10 +13,10 @@ public class AsyncMethodService { @Async public void doSomethingAsync() { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "key1", "should be cleared from metadata"); + Bugsnag.clearThreadMetadata(); + Bugsnag.addThreadMetadata("thread", "key2", "should be included in metadata"); try { Thread.sleep(100); diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java index 3574ff08..dd9eb8d8 100644 --- a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java @@ -17,10 +17,10 @@ public class ScheduledTaskService { public void doSomething() { if (throwException && !exceptionSent) { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "key1", "should be cleared from metadata"); + Bugsnag.clearThreadMetadata(); + Bugsnag.addThreadMetadata("thread", "key2", "should be included in metadata"); exceptionSent = true; throw new RuntimeException("Unhandled exception from ScheduledTaskService"); diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java index 5e0593a0..41ed6727 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java @@ -15,10 +15,10 @@ public class AsyncMethodService { @Async public void doSomethingAsync() { - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "key1", "should be cleared from metadata"); + Bugsnag.clearThreadMetadata(); + Bugsnag.addThreadMetadata("thread", "key2", "should be included in metadata"); try { Thread.sleep(100); @@ -32,7 +32,7 @@ public void doSomethingAsync() { @Async public void notifyAsync() { // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "inAsyncMethod", "meta data from async method"); + Bugsnag.addThreadMetadata("thread", "inAsyncMethod", "meta data from async method"); bugsnag.notify(new RuntimeException("test from async")); } diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java index db4937ec..5889e122 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java @@ -25,10 +25,10 @@ public void doSomething() { // Reset the flag so we only throw once throwException = false; - // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); - Bugsnag.clearThreadMetaData(); - Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + // Add some thread metadata + Bugsnag.addThreadMetadata("thread", "key1", "should be cleared from metadata"); + Bugsnag.clearThreadMetadata(); + Bugsnag.addThreadMetadata("thread", "key2", "should be included in metadata"); throw new RuntimeException("Unhandled exception from ScheduledTaskService"); } diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java index e35c0524..c8667ffb 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java @@ -57,7 +57,7 @@ public String runAsyncTask() { public String notifyAsyncTask() { // Add some thread meta data - Bugsnag.addThreadMetaData("thread", "controllerMethod", "meta data from controller method"); + Bugsnag.addThreadMetadata("thread", "controllerMethod", "meta data from controller method"); // Notify before calling the async method bugsnag.notify(new RuntimeException("test from before async")); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetaDataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java similarity index 79% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetaDataScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java index 698bee5a..37bd1ad7 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetaDataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java @@ -9,13 +9,13 @@ import org.slf4j.LoggerFactory; /** - * Sends an exception to Bugsnag with custom meta data using the logback appender + * Sends an exception to Bugsnag with custom metadata using the logback appender */ -public class LogbackMetaDataScenario extends Scenario { +public class LogbackMetadataScenario extends Scenario { - private static final Logger LOGGER = LoggerFactory.getLogger(LogbackMetaDataScenario.class); + private static final Logger LOGGER = LoggerFactory.getLogger(LogbackMetadataScenario.class); - public LogbackMetaDataScenario(Bugsnag bugsnag) { + public LogbackMetadataScenario(Bugsnag bugsnag) { super(bugsnag); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackThreadMetaDataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackThreadMetadataScenario.java similarity index 62% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackThreadMetaDataScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackThreadMetadataScenario.java index b2ea7d40..04c34da5 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackThreadMetaDataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackThreadMetadataScenario.java @@ -7,27 +7,27 @@ import org.slf4j.LoggerFactory; /** - * Sends an exception to Bugsnag with custom meta data using the logback appender + * Sends an exception to Bugsnag with custom metadata using the logback appender */ -public class LogbackThreadMetaDataScenario extends Scenario { +public class LogbackThreadMetadataScenario extends Scenario { private static final Logger LOGGER = - LoggerFactory.getLogger(LogbackThreadMetaDataScenario.class); + LoggerFactory.getLogger(LogbackThreadMetadataScenario.class); - public LogbackThreadMetaDataScenario(Bugsnag bugsnag) { + public LogbackThreadMetadataScenario(Bugsnag bugsnag) { super(bugsnag); } @Override public void run() { - Bugsnag.addThreadMetaData("thread", "foo", "threadvalue1"); - Bugsnag.addThreadMetaData("thread", "bar", "threadvalue2"); + Bugsnag.addThreadMetadata("thread", "foo", "threadvalue1"); + Bugsnag.addThreadMetadata("thread", "bar", "threadvalue2"); // Thread metadata on a different thread should not get added Thread t1 = new Thread(new Runnable() { @Override public void run() { - Bugsnag.addThreadMetaData("Custom", "something", "This should not be on the report"); + Bugsnag.addThreadMetadata("Custom", "something", "This should not be on the report"); } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetaDataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java similarity index 84% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetaDataScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java index 63f2616a..e639c540 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetaDataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java @@ -7,9 +7,9 @@ /** * Sends a handled exception to Bugsnag, which includes custom metadata */ -public class MetaDataScenario extends Scenario { +public class MetadataScenario extends Scenario { - public MetaDataScenario(Bugsnag bugsnag) { + public MetadataScenario(Bugsnag bugsnag) { super(bugsnag); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetaDataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java similarity index 79% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetaDataScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java index 8a6a5c35..254718fa 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetaDataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java @@ -5,11 +5,11 @@ import com.bugsnag.Report; /** - * Sends an exception to Bugsnag with custom meta data on the thread + * Sends an exception to Bugsnag with custom metadata on the thread */ -public class ThreadMetaDataScenario extends Scenario { +public class ThreadMetadataScenario extends Scenario { - public ThreadMetaDataScenario(Bugsnag bugsnag) { + public ThreadMetadataScenario(Bugsnag bugsnag) { super(bugsnag); } @@ -25,14 +25,14 @@ public void beforeNotify(Report report) { }); // Thread metadata should merge with global metadata and overwrite when duplicate key - Bugsnag.addThreadMetaData("Custom", "foo", "Thread value"); - Bugsnag.addThreadMetaData("Custom", "bar", "Thread value to be overwritten"); + Bugsnag.addThreadMetadata("Custom", "foo", "Thread value"); + Bugsnag.addThreadMetadata("Custom", "bar", "Thread value to be overwritten"); // Thread metadata on a different thread should not get added Thread t1 = new Thread(new Runnable() { @Override public void run() { - Bugsnag.addThreadMetaData("Custom", "something", "This should not be on the report"); + Bugsnag.addThreadMetadata("Custom", "something", "This should not be on the report"); } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetaDataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java similarity index 74% rename from features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetaDataScenario.java rename to features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java index 1d8c4a45..c8c3dbba 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetaDataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java @@ -5,11 +5,11 @@ import com.bugsnag.Report; /** - * Sends an unhandled exception to Bugsnag with custom meta data on the thread + * Sends an unhandled exception to Bugsnag with custom metadata on the thread */ -public class UnhandledThreadMetaDataScenario extends Scenario { +public class UnhandledThreadMetadataScenario extends Scenario { - public UnhandledThreadMetaDataScenario(Bugsnag bugsnag) { + public UnhandledThreadMetadataScenario(Bugsnag bugsnag) { super(bugsnag); } @@ -28,7 +28,7 @@ public void beforeNotify(Report report) { Thread t1 = new Thread(new Runnable() { @Override public void run() { - Bugsnag.addThreadMetaData("Custom", "something", "This should not be on the report"); + Bugsnag.addThreadMetadata("Custom", "something", "This should not be on the report"); } }); t1.start(); @@ -43,9 +43,9 @@ public void run() { @Override public void run() { // Thread metadata should merge with global metadata and overwrite when duplicate key - Bugsnag.addThreadMetaData("Custom", "foo", "Thread value 1"); - Bugsnag.addThreadMetaData("Custom", "bar", "Thread value 2"); - throw new RuntimeException("UnhandledThreadMetaDataScenario"); + Bugsnag.addThreadMetadata("Custom", "foo", "Thread value 1"); + Bugsnag.addThreadMetadata("Custom", "bar", "Thread value 2"); + throw new RuntimeException("UnhandledThreadMetadataScenario"); } }); t2.start(); diff --git a/features/meta_data.feature b/features/meta_data.feature index 7d268c5b..a77afff2 100644 --- a/features/meta_data.feature +++ b/features/meta_data.feature @@ -1,24 +1,24 @@ Feature: Reporting metadata Scenario: Sends a handled exception which includes custom metadata added in a notify callback - When I run "MetaDataScenario" with the defaults + When I run "MetadataScenario" 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 event "metaData.Custom.foo" equals "Hello World!" Scenario: Sends a handled exception which includes custom metadata added in a notify callback for Spring Boot app - When I run spring boot "MetaDataScenario" with the defaults + When I run spring boot "MetadataScenario" 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 event "metaData.Custom.foo" equals "Hello World!" Scenario: Sends a handled exception which includes custom metadata added in a notify callback for plain Spring app - When I run plain Spring "MetaDataScenario" with the defaults + When I run plain Spring "MetadataScenario" 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 event "metaData.Custom.foo" equals "Hello World!" -Scenario: Test logback appender with meta data in the config file +Scenario: Test logback appender with metadata in the config file When I run "LogbackScenario" with logback config "meta_data_config.xml" And I wait to receive an error And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier @@ -27,8 +27,8 @@ Scenario: Test logback appender with meta data in the config file And the event "metaData.configTab.foo" equals "tabValue1" And the event "metaData.configTab.bar" equals "tabValue2" -Scenario: Test logback appender with thread meta data - When I run "LogbackThreadMetaDataScenario" with logback config "basic_config.xml" +Scenario: Test logback appender with thread metadata + When I run "LogbackThreadMetadataScenario" with logback config "basic_config.xml" 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 @@ -36,8 +36,8 @@ Scenario: Test logback appender with thread meta data And the event "metaData.thread.foo" equals "threadvalue1" And the event "metaData.thread.bar" equals "threadvalue2" -Scenario: Test logback appender with meta data in a callback - When I run "LogbackMetaDataScenario" with logback config "basic_config.xml" +Scenario: Test logback appender with metadata in a callback + When I run "LogbackMetadataScenario" with logback config "basic_config.xml" 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 @@ -46,7 +46,7 @@ Scenario: Test logback appender with meta data in a callback And the event "metaData.custom.foo" equals "hunter2" And the event "metaData.custom.bar" equals "hunter2" -Scenario: Test logback appender with meta data from the MDC +Scenario: Test logback appender with metadata from the MDC When I run "LogbackMDCScenario" with logback config "basic_config.xml" And I wait to receive an error And the error is valid for the error reporting API version "4" for the "Bugsnag Java" notifier @@ -55,8 +55,8 @@ Scenario: Test logback appender with meta data from the MDC And the event "metaData.Context.foo" equals "hunter2" And the event "metaData.Context.bar" equals "hunter2" -Scenario: Test thread meta data - When I run "ThreadMetaDataScenario" with the defaults +Scenario: Test thread metadata + When I run "ThreadMetadataScenario" 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 @@ -66,8 +66,8 @@ Scenario: Test thread meta data And the event "metaData.Custom.bar" equals "Hello World!" And the event "metaData.Custom.something" is null -Scenario: Test thread meta data for Spring Boot app - When I run spring boot "ThreadMetaDataScenario" with the defaults +Scenario: Test thread metadata for Spring Boot app + When I run spring boot "ThreadMetadataScenario" 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 @@ -77,8 +77,8 @@ Scenario: Test thread meta data for Spring Boot app And the event "metaData.Custom.bar" equals "Hello World!" And the event "metaData.Custom.something" is null -Scenario: Test thread meta data for plain Spring app - When I run plain Spring "ThreadMetaDataScenario" with the defaults +Scenario: Test thread metadata for plain Spring app + When I run plain Spring "ThreadMetadataScenario" 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 @@ -88,8 +88,8 @@ Scenario: Test thread meta data for plain Spring app And the event "metaData.Custom.bar" equals "Hello World!" And the event "metaData.Custom.something" is null -Scenario: Test unhandled thread meta data - When I run "UnhandledThreadMetaDataScenario" with the defaults +Scenario: Test unhandled thread metadata + When I run "UnhandledThreadMetadataScenario" 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 @@ -99,8 +99,8 @@ Scenario: Test unhandled thread meta data And the event "metaData.Custom.bar" equals "Thread value 2" And the event "metaData.Custom.something" is null -Scenario: Test unhandled thread meta data for Spring Boot app - When I run spring boot "UnhandledThreadMetaDataScenario" with the defaults +Scenario: Test unhandled thread metadata for Spring Boot app + When I run spring boot "UnhandledThreadMetadataScenario" 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 @@ -110,8 +110,8 @@ Scenario: Test unhandled thread meta data for Spring Boot app And the event "metaData.Custom.bar" equals "Thread value 2" And the event "metaData.Custom.something" is null -Scenario: Test unhandled thread meta data for plain Spring app - When I run plain Spring "UnhandledThreadMetaDataScenario" with the defaults +Scenario: Test unhandled thread metadata for plain Spring app + When I run plain Spring "UnhandledThreadMetadataScenario" 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 @@ -121,8 +121,8 @@ Scenario: Test unhandled thread meta data for plain Spring app And the event "metaData.Custom.bar" equals "Thread value 2" And the event "metaData.Custom.something" is null -Scenario: Test logback appender with thread meta data - When I run "LogbackThreadMetaDataScenario" with logback config "basic_config.xml" +Scenario: Test logback appender with thread metadata + When I run "LogbackThreadMetadataScenario" with logback config "basic_config.xml" 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 @@ -137,14 +137,14 @@ Scenario: Test thread meta data in plain spring async method 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 event "metaData.thread.key1" is null - And the event "metaData.thread.key2" equals "should be included in meta data" + And the event "metaData.thread.key2" equals "should be included in metadata" Scenario: Test thread meta data in spring boot async method When I run spring boot "AsyncMethodScenario" 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 event "metaData.thread.key1" is null - And the event "metaData.thread.key2" equals "should be included in meta data" + And the event "metaData.thread.key2" equals "should be included in metadata" Scenario: Test thread meta data in plain spring scheduled task Given I set environment variable "RUN_SCHEDULED_TASK" to "true" @@ -152,11 +152,11 @@ Scenario: Test thread meta data in plain spring scheduled task 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 event "metaData.thread.key1" is null - And the event "metaData.thread.key2" equals "should be included in meta data" + And the event "metaData.thread.key2" equals "should be included in metadata" Scenario: Test thread meta data in spring boot scheduled task When I run spring boot "ScheduledTaskScenario" 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 event "metaData.thread.key1" is null - And the event "metaData.thread.key2" equals "should be included in meta data" + And the event "metaData.thread.key2" equals "should be included in metadata" diff --git a/features/redacting_metadata.feature b/features/redacting_metadata.feature index e3d74a3d..9585d111 100644 --- a/features/redacting_metadata.feature +++ b/features/redacting_metadata.feature @@ -19,18 +19,18 @@ Scenario: Adding a custom metadata redaction And the event "metaData.custom.bar" equals "hunter2" Scenario: Adding a thread metadata redaction using logback - When I run "LogbackThreadMetaDataScenario" with logback config "meta_data_redact_config.xml" + When I run "LogbackThreadMetadataScenario" with logback config "meta_data_redact_config.xml" 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 exception "message" equals "LogbackThreadMetaDataScenario" + And the exception "message" equals "LogbackThreadMetadataScenario" And the event "metaData.thread.foo" equals "[REDACTED]" And the event "metaData.thread.bar" equals "threadvalue2" Scenario: Adding a custom metadata redaction using logback - When I run "LogbackMetaDataScenario" with logback config "meta_data_redact_config.xml" + When I run "LogbackMetadataScenario" with logback config "meta_data_redact_config.xml" 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 exception "message" equals "LogbackMetaDataScenario" + And the exception "message" equals "LogbackMetadataScenario" And the event "metaData.custom.foo" equals "[REDACTED]" And the event "metaData.user.foo" equals "[REDACTED]" And the event "metaData.custom.bar" equals "hunter2" From b530c39ff2d868f34ca56cd5df407c08cbf1936f Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Thu, 13 Nov 2025 10:59:33 +0100 Subject: [PATCH 05/10] notifyReleaseStages -> enabledReleaseStages (#239) --- .../src/main/java/com/bugsnag/Bugsnag.java | 21 ++++++++----- .../java/com/bugsnag/BugsnagAppender.java | 31 +++++++++++-------- .../main/java/com/bugsnag/Configuration.java | 11 +++---- .../test/java/com/bugsnag/AppenderTest.java | 16 ++++------ .../test/java/com/bugsnag/BugsnagTest.java | 13 ++++---- .../java/com/bugsnag/SessionTrackerTest.java | 5 +-- bugsnag/src/test/resources/logback.xml | 4 +-- .../logback/src/main/resources/logback.xml | 4 +-- .../logback/ignore_release_stage_config.xml | 4 +-- .../ArrayNotifyReleaseStageScenario.java | 2 +- .../scenarios/InsideReleaseStageScenario.java | 2 +- .../NullNotifyReleaseStageScenario.java | 3 +- .../scenarios/NullReleaseStageScenario.java | 2 +- .../OutsideReleaseStageScenario.java | 2 +- 14 files changed, 64 insertions(+), 56 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 82ba35a0..913a9a2f 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -253,11 +253,15 @@ public void setIgnoreClasses(String... ignoreClasses) { * Set for which releaseStages errors should be sent to Bugsnag. * Use this to stop errors from development builds being sent. * - * @param notifyReleaseStages a list of releaseStages to notify for + * @param enabledReleaseStages a list of releaseStages to notify for * @see #setReleaseStage */ - public void setNotifyReleaseStages(String... notifyReleaseStages) { - config.notifyReleaseStages = notifyReleaseStages; + public void setEnabledReleaseStages(String... enabledReleaseStages) { + if (enabledReleaseStages == null || enabledReleaseStages.length == 0) { + config.enabledReleaseStages = Collections.emptySet(); + } else { + config.enabledReleaseStages = Set.of(enabledReleaseStages); + } } /** @@ -289,7 +293,7 @@ public void setProxy(Proxy proxy) { * Set the current "release stage" of your application. * * @param releaseStage the release stage of the app - * @see #setNotifyReleaseStages + * @see #setEnabledReleaseStages */ public void setReleaseStage(String releaseStage) { config.releaseStage = releaseStage; @@ -302,14 +306,15 @@ public void setReleaseStage(String releaseStage) { * environment. * * @param sendThreads should we send thread state with error reports - * @see #setNotifyReleaseStages + * @see #setEnabledReleaseStages */ public void setSendThreads(boolean sendThreads) { config.sendThreads = sendThreads; } /** - * Set a timeout (in ms) to use when delivering Bugsnag error reports and sessions. + * Set a timeout (in ms) to use when delivering Bugsnag error reports and + * sessions. * This is a convenient shorthand for bugsnag.getDelivery().setTimeout(); * * @param timeout the timeout to set (in ms) @@ -440,9 +445,9 @@ public boolean notify(Report report, Callback reportCallback) { return false; } - // Don't notify unless releaseStage is in notifyReleaseStages + // Don't notify unless releaseStage is in enabledReleaseStages if (!config.shouldNotifyForReleaseStage()) { - LOGGER.debug("Error not reported to Bugsnag - {} is not in 'notifyReleaseStages'", + LOGGER.debug("Error not reported to Bugsnag - {} is not in 'enabledReleaseStages'", config.releaseStage); return false; } diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index cc99c42e..7fc17c46 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -55,7 +55,7 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase { private Set ignoredClasses = new HashSet(); /** Release stages that should be notified. */ - private Set notifyReleaseStages = new HashSet(); + private Set enabledReleaseStages = new HashSet(); /** Project packages. */ private Set projectPackages = new HashSet(); @@ -254,20 +254,20 @@ private Bugsnag createBugsnag() { bugsnag.setTimeout(timeout); } - if (redactedKeys.size() > 0) { + if (!redactedKeys.isEmpty()) { bugsnag.setRedactedKeys(redactedKeys.toArray(new String[0])); } bugsnag.setIgnoreClasses(ignoredClasses.toArray(new String[0])); - if (notifyReleaseStages.size() > 0) { - bugsnag.setNotifyReleaseStages(notifyReleaseStages.toArray(new String[0])); + if (!enabledReleaseStages.isEmpty()) { + bugsnag.setEnabledReleaseStages(enabledReleaseStages.toArray(new String[0])); } bugsnag.setProjectPackages(projectPackages.toArray(new String[0])); bugsnag.setSendThreads(sendThreads); - // Add a callback to put global meta data on every report + // Add a callback to put global metadata on every report bugsnag.addCallback(new Callback() { @Override public void beforeNotify(Report report) { @@ -417,25 +417,30 @@ public void setIgnoredClasses(String ignoredClasses) { } } + @Deprecated + public void setNotifyReleaseStage(String notifyReleaseStage) { + setEnabledReleaseStage(notifyReleaseStage); + } + /** - * @see Bugsnag#setNotifyReleaseStages(String...) + * @see Bugsnag#setEnabledReleaseStages(String...) */ - public void setNotifyReleaseStage(String notifyReleaseStage) { - this.notifyReleaseStages.add(notifyReleaseStage); + public void setEnabledReleaseStage(String enabledReleaseStage) { + this.enabledReleaseStages.add(enabledReleaseStage); if (bugsnag != null) { - bugsnag.setNotifyReleaseStages(this.notifyReleaseStages.toArray(new String[0])); + bugsnag.setEnabledReleaseStages(this.enabledReleaseStages.toArray(new String[0])); } } /** - * @see Bugsnag#setNotifyReleaseStages(String...) + * @see Bugsnag#setEnabledReleaseStages(String...) */ - public void setNotifyReleaseStages(String notifyReleaseStages) { - this.notifyReleaseStages.addAll(split(notifyReleaseStages)); + public void setEnabledReleaseStages(String enabledReleaseStages) { + this.enabledReleaseStages.addAll(split(enabledReleaseStages)); if (bugsnag != null) { - bugsnag.setNotifyReleaseStages(this.notifyReleaseStages.toArray(new String[0])); + bugsnag.setEnabledReleaseStages(this.enabledReleaseStages.toArray(new String[0])); } } diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index c3b5c7d4..e004c309 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,9 +37,9 @@ public class Configuration { public Delivery delivery; public EndpointConfiguration endpointConfiguration; public Delivery sessionDelivery; - public String[] redactedKeys = new String[]{"password", "secret", "Authorization", "Cookie"}; + public String[] redactedKeys = new String[] {"password", "secret", "Authorization", "Cookie"}; public String[] ignoreClasses; - public String[] notifyReleaseStages = null; + public Set enabledReleaseStages = null; public String[] projectPackages; public String releaseStage; public boolean sendThreads = false; @@ -66,12 +67,10 @@ public class Configuration { } boolean shouldNotifyForReleaseStage() { - if (notifyReleaseStages == null) { + if (enabledReleaseStages == null) { return true; } - - List stages = Arrays.asList(notifyReleaseStages); - return stages.contains(releaseStage); + return enabledReleaseStages.contains(releaseStage); } boolean shouldIgnoreClass(String className) { diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index 34c65ef0..a97c3648 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -150,20 +150,16 @@ public void testBugsnagConfig() { assertTrue(redactedKeys.contains("credit_card_number")); assertEquals(2, config.ignoreClasses.length); - ArrayList ignoreClasses - = new ArrayList(Arrays.asList(config.ignoreClasses)); + ArrayList ignoreClasses = new ArrayList(Arrays.asList(config.ignoreClasses)); assertTrue(ignoreClasses.contains("com.example.Custom")); assertTrue(ignoreClasses.contains("java.io.IOException")); - assertEquals(2, config.notifyReleaseStages.length); - ArrayList notifyReleaseStages - = new ArrayList(Arrays.asList(config.notifyReleaseStages)); - assertTrue(notifyReleaseStages.contains("development")); - assertTrue(notifyReleaseStages.contains("test")); + assertEquals(2, config.enabledReleaseStages.size()); + assertTrue(config.enabledReleaseStages.contains("development")); + assertTrue(config.enabledReleaseStages.contains("test")); assertEquals(2, config.projectPackages.length); - ArrayList projectPackages - = new ArrayList(Arrays.asList(config.projectPackages)); + ArrayList projectPackages = new ArrayList(Arrays.asList(config.projectPackages)); assertTrue(projectPackages.contains("com.company.package2")); assertTrue(projectPackages.contains("com.company.package1")); @@ -208,7 +204,7 @@ public void testIgnoreClasses() { } @Test - public void testNotifyReleaseStages() { + public void testEnabledReleaseStages() { // Send a log with the release stage set to an excluded one appender.setReleaseStage("ignoredReleaseStage"); LOGGER.warn("Release stage ignored", new RuntimeException("test")); diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index 4b9443d9..0b337a9f 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -17,13 +17,14 @@ import org.junit.Test; import java.io.ByteArrayOutputStream; + import java.net.InetSocketAddress; import java.net.Proxy; + import java.util.HashMap; import java.util.Map; import java.util.Set; - public class BugsnagTest { private Bugsnag bugsnag; @@ -73,25 +74,25 @@ public void testIgnoreClasses() { } @Test - public void testNotifyReleaseStages() { + public void testEnabledReleaseStages() { bugsnag.setDelivery(BugsnagTestUtils.generateDelivery()); bugsnag.setReleaseStage("production"); // Never send - bugsnag.setNotifyReleaseStages(); + bugsnag.setEnabledReleaseStages(); assertFalse(bugsnag.notify(new Throwable())); // Ignore 'production' - bugsnag.setNotifyReleaseStages("staging", "development"); + bugsnag.setEnabledReleaseStages("staging"); assertFalse(bugsnag.notify(new Throwable())); // Allow 'production' - bugsnag.setNotifyReleaseStages("production"); + bugsnag.setEnabledReleaseStages("production"); assertTrue(bugsnag.notify(new Throwable())); // Allow 'production' and others - bugsnag.setNotifyReleaseStages("production", "staging", "development"); + bugsnag.setEnabledReleaseStages("production"); assertTrue(bugsnag.notify(new Throwable())); } diff --git a/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java b/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java index 65d71ba3..d1dcf9ec 100644 --- a/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java +++ b/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java @@ -14,6 +14,7 @@ import org.junit.Before; import org.junit.Test; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -133,7 +134,7 @@ public void run() { @Test public void disabledReleaseStage() { - configuration.notifyReleaseStages = new String[]{"prod"}; + configuration.enabledReleaseStages = Collections.singleton("prod"); configuration.releaseStage = "dev"; sessionTracker.startSession(new Date(), false); assertNull(sessionTracker.getSession()); @@ -141,7 +142,7 @@ public void disabledReleaseStage() { @Test public void enabledReleaseStage() { - configuration.notifyReleaseStages = new String[]{"prod"}; + configuration.enabledReleaseStages = Collections.singleton("prod"); configuration.releaseStage = "prod"; sessionTracker.startSession(new Date(), false); assertNotNull(sessionTracker.getSession()); diff --git a/bugsnag/src/test/resources/logback.xml b/bugsnag/src/test/resources/logback.xml index 40270aa5..dd2337c8 100644 --- a/bugsnag/src/test/resources/logback.xml +++ b/bugsnag/src/test/resources/logback.xml @@ -13,8 +13,8 @@ java.io.IOException com.example.Custom - test - development + test + development com.company.package1 com.company.package2 diff --git a/examples/logback/src/main/resources/logback.xml b/examples/logback/src/main/resources/logback.xml index a62e51a0..3eefdaf8 100644 --- a/examples/logback/src/main/resources/logback.xml +++ b/examples/logback/src/main/resources/logback.xml @@ -23,8 +23,8 @@ - - + + diff --git a/features/fixtures/logback/ignore_release_stage_config.xml b/features/fixtures/logback/ignore_release_stage_config.xml index 95b1da88..b8bf5d4c 100644 --- a/features/fixtures/logback/ignore_release_stage_config.xml +++ b/features/fixtures/logback/ignore_release_stage_config.xml @@ -7,8 +7,8 @@ staging 1.0.0 - production - development + production + development http://localhost:9339/notify diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ArrayNotifyReleaseStageScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ArrayNotifyReleaseStageScenario.java index 9aa0ac58..b6225eec 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ArrayNotifyReleaseStageScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ArrayNotifyReleaseStageScenario.java @@ -14,7 +14,7 @@ public ArrayNotifyReleaseStageScenario(Bugsnag bugsnag) { @Override public void run() { bugsnag.setReleaseStage("prod"); - bugsnag.setNotifyReleaseStages("dev", "prod"); + bugsnag.setEnabledReleaseStages("dev", "prod"); bugsnag.notify(generateException()); } } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/InsideReleaseStageScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/InsideReleaseStageScenario.java index fbe7d13f..9eb129db 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/InsideReleaseStageScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/InsideReleaseStageScenario.java @@ -14,7 +14,7 @@ public InsideReleaseStageScenario(Bugsnag bugsnag) { @Override public void run() { bugsnag.setReleaseStage("prod"); - bugsnag.setNotifyReleaseStages("prod"); + bugsnag.setEnabledReleaseStages("prod"); bugsnag.notify(generateException()); } } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullNotifyReleaseStageScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullNotifyReleaseStageScenario.java index af4338b0..1d0e6255 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullNotifyReleaseStageScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullNotifyReleaseStageScenario.java @@ -3,7 +3,8 @@ import com.bugsnag.Bugsnag; /** - * Attempts to send a handled exception to Bugsnag, when the notifyReleaseStages is null. + * Attempts to send a handled exception to Bugsnag, when the + * enabledReleaseStages is null. */ public class NullNotifyReleaseStageScenario extends Scenario { diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullReleaseStageScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullReleaseStageScenario.java index ae4b2c1f..7875c84d 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullReleaseStageScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/NullReleaseStageScenario.java @@ -14,7 +14,7 @@ public NullReleaseStageScenario(Bugsnag bugsnag) { @Override public void run() { bugsnag.setReleaseStage(null); - bugsnag.setNotifyReleaseStages("dev"); + bugsnag.setEnabledReleaseStages("dev"); bugsnag.notify(generateException()); } } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/OutsideReleaseStageScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/OutsideReleaseStageScenario.java index bd35ee73..efc8103d 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/OutsideReleaseStageScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/OutsideReleaseStageScenario.java @@ -15,7 +15,7 @@ public OutsideReleaseStageScenario(Bugsnag bugsnag) { @Override public void run() { bugsnag.setReleaseStage("prod"); - bugsnag.setNotifyReleaseStages("dev"); + bugsnag.setEnabledReleaseStages("dev"); bugsnag.notify(generateException()); } } From 4de3d6a68e7ce6b44642fdf30a3662a811cb241a Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Thu, 13 Nov 2025 15:48:30 +0100 Subject: [PATCH 06/10] rename ignoreClasses to discardClasses (#240) --- .../src/main/java/com/bugsnag/Bugsnag.java | 8 +++--- .../java/com/bugsnag/BugsnagAppender.java | 25 +++++++++++-------- .../main/java/com/bugsnag/Configuration.java | 6 ++--- .../test/java/com/bugsnag/AppenderTest.java | 8 +++--- .../test/java/com/bugsnag/BugsnagTest.java | 6 ++--- bugsnag/src/test/resources/logback.xml | 4 +-- .../logback/src/main/resources/logback.xml | 4 +-- .../fixtures/logback/ignored_class_config.xml | 2 +- .../scenarios/IgnoredExceptionScenario.java | 2 +- 9 files changed, 35 insertions(+), 30 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 913a9a2f..635a0439 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -243,10 +243,10 @@ public void setRedactedKeys(String... redactedKeys) { /** * Set which exception classes should be ignored (not sent) by Bugsnag. * - * @param ignoreClasses a list of exception classes to ignore + * @param discardClasses a list of exception classes to ignore */ - public void setIgnoreClasses(String... ignoreClasses) { - config.ignoreClasses = ignoreClasses; + public void setDiscardClasses(String... discardClasses) { + config.discardClasses = discardClasses; } /** @@ -440,7 +440,7 @@ public boolean notify(Report report, Callback reportCallback) { // Don't notify if this error class should be ignored if (config.shouldIgnoreClass(report.getExceptionName())) { - LOGGER.debug("Error not reported to Bugsnag - {} is in 'ignoreClasses'", + LOGGER.debug("Error not reported to Bugsnag - {} is in 'discardClasses'", report.getExceptionName()); return false; } diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 7fc17c46..c65df317 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -52,7 +52,7 @@ public class BugsnagAppender extends UnsynchronizedAppenderBase { private Set redactedKeys = new HashSet(); /** Exception classes to be ignored. */ - private Set ignoredClasses = new HashSet(); + private Set discardClasses = new HashSet(); /** Release stages that should be notified. */ private Set enabledReleaseStages = new HashSet(); @@ -258,7 +258,7 @@ private Bugsnag createBugsnag() { bugsnag.setRedactedKeys(redactedKeys.toArray(new String[0])); } - bugsnag.setIgnoreClasses(ignoredClasses.toArray(new String[0])); + bugsnag.setDiscardClasses(discardClasses.toArray(new String[0])); if (!enabledReleaseStages.isEmpty()) { bugsnag.setEnabledReleaseStages(enabledReleaseStages.toArray(new String[0])); @@ -395,25 +395,30 @@ public void setRedactedKeys(String key) { } } + @Deprecated + public void setIgnoredClass(String ignoredClass) { + setDiscardClass(ignoredClass); + } + /** - * @see Bugsnag#setIgnoreClasses(String...) + * @see Bugsnag#setDiscardClasses(String...) */ - public void setIgnoredClass(String ignoredClass) { - this.ignoredClasses.add(ignoredClass); + public void setDiscardClass(String discardClass) { + this.discardClasses.add(discardClass); if (bugsnag != null) { - bugsnag.setIgnoreClasses(this.ignoredClasses.toArray(new String[0])); + bugsnag.setDiscardClasses(this.discardClasses.toArray(new String[0])); } } /** - * @see Bugsnag#setIgnoreClasses(String...) + * @see Bugsnag#setDiscardClasses(String...) */ - public void setIgnoredClasses(String ignoredClasses) { - this.ignoredClasses.addAll(split(ignoredClasses)); + public void setDiscardClasses(String discardClasses) { + this.discardClasses.addAll(split(discardClasses)); if (bugsnag != null) { - bugsnag.setIgnoreClasses(this.ignoredClasses.toArray(new String[0])); + bugsnag.setDiscardClasses(this.discardClasses.toArray(new String[0])); } } diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index e004c309..d7652574 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -38,7 +38,7 @@ public class Configuration { public EndpointConfiguration endpointConfiguration; public Delivery sessionDelivery; public String[] redactedKeys = new String[] {"password", "secret", "Authorization", "Cookie"}; - public String[] ignoreClasses; + public String[] discardClasses; public Set enabledReleaseStages = null; public String[] projectPackages; public String releaseStage; @@ -74,11 +74,11 @@ boolean shouldNotifyForReleaseStage() { } boolean shouldIgnoreClass(String className) { - if (ignoreClasses == null) { + if (discardClasses == null) { return false; } - List classes = Arrays.asList(ignoreClasses); + List classes = Arrays.asList(discardClasses); return classes.contains(className); } diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index a97c3648..248e50e1 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -149,10 +149,10 @@ public void testBugsnagConfig() { assertTrue(redactedKeys.contains("password")); assertTrue(redactedKeys.contains("credit_card_number")); - assertEquals(2, config.ignoreClasses.length); - ArrayList ignoreClasses = new ArrayList(Arrays.asList(config.ignoreClasses)); - assertTrue(ignoreClasses.contains("com.example.Custom")); - assertTrue(ignoreClasses.contains("java.io.IOException")); + assertEquals(2, config.discardClasses.length); + ArrayList discardClasses = new ArrayList(Arrays.asList(config.discardClasses)); + assertTrue(discardClasses.contains("com.example.Custom")); + assertTrue(discardClasses.contains("java.io.IOException")); assertEquals(2, config.enabledReleaseStages.size()); assertTrue(config.enabledReleaseStages.contains("development")); diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index 0b337a9f..e075be43 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -58,17 +58,17 @@ public void testIgnoreClasses() { bugsnag.setDelivery(BugsnagTestUtils.generateDelivery()); // Ignore neither - bugsnag.setIgnoreClasses(); + bugsnag.setDiscardClasses(); assertTrue(bugsnag.notify(new RuntimeException())); assertTrue(bugsnag.notify(new TestException())); // Ignore just RuntimeException - bugsnag.setIgnoreClasses(RuntimeException.class.getName()); + bugsnag.setDiscardClasses(RuntimeException.class.getName()); assertFalse(bugsnag.notify(new RuntimeException())); assertTrue(bugsnag.notify(new TestException())); // Ignore both - bugsnag.setIgnoreClasses(RuntimeException.class.getName(), TestException.class.getName()); + bugsnag.setDiscardClasses(RuntimeException.class.getName(), TestException.class.getName()); assertFalse(bugsnag.notify(new RuntimeException())); assertFalse(bugsnag.notify(new TestException())); } diff --git a/bugsnag/src/test/resources/logback.xml b/bugsnag/src/test/resources/logback.xml index dd2337c8..109091af 100644 --- a/bugsnag/src/test/resources/logback.xml +++ b/bugsnag/src/test/resources/logback.xml @@ -10,8 +10,8 @@ password credit_card_number - java.io.IOException - com.example.Custom + java.io.IOException + com.example.Custom test development diff --git a/examples/logback/src/main/resources/logback.xml b/examples/logback/src/main/resources/logback.xml index 3eefdaf8..ae127ffb 100644 --- a/examples/logback/src/main/resources/logback.xml +++ b/examples/logback/src/main/resources/logback.xml @@ -20,8 +20,8 @@ - - + + diff --git a/features/fixtures/logback/ignored_class_config.xml b/features/fixtures/logback/ignored_class_config.xml index 75577a21..fb425538 100644 --- a/features/fixtures/logback/ignored_class_config.xml +++ b/features/fixtures/logback/ignored_class_config.xml @@ -7,7 +7,7 @@ production 1.0.0 - java.lang.RuntimeException + java.lang.RuntimeException 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 f2612590..ef0dfc3d 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 @@ -15,7 +15,7 @@ public IgnoredExceptionScenario(Bugsnag bugsnag) { @Override public void run() { - bugsnag.setIgnoreClasses("java.lang.RuntimeException"); + bugsnag.setDiscardClasses("java.lang.RuntimeException"); bugsnag.notify(new RuntimeException("Should never appear")); } From a5310759a76682b9d8312c2ca786d5fb8f9fe2b6 Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Mon, 24 Nov 2025 09:34:17 +0100 Subject: [PATCH 07/10] rename callbacks and update functionality (#241) --- .../bugsnag/BugsnagSpringConfiguration.java | 14 +-- .../com/bugsnag/ExceptionClassCallback.java | 5 +- .../com/bugsnag/SpringBootConfiguration.java | 14 +-- .../test/java/com/bugsnag/SpringMvcTest.java | 3 +- .../testapp/springboot/TestController.java | 3 +- .../java/com/bugsnag/BeforeSendSession.java | 5 - .../src/main/java/com/bugsnag/Bugsnag.java | 28 ++---- .../java/com/bugsnag/BugsnagAppender.java | 11 ++- .../src/main/java/com/bugsnag/OnSession.java | 6 ++ .../main/java/com/bugsnag/SessionPayload.java | 15 ++- .../main/java/com/bugsnag/SessionTracker.java | 47 ++++++--- .../com/bugsnag/callbacks/AppCallback.java | 3 +- .../java/com/bugsnag/callbacks/Callback.java | 6 +- .../com/bugsnag/callbacks/DeviceCallback.java | 3 +- .../callbacks/JakartaServletCallback.java | 5 +- .../com/bugsnag/AppenderMetadataTest.java | 10 +- .../test/java/com/bugsnag/AppenderTest.java | 20 ++-- .../test/java/com/bugsnag/BugsnagTest.java | 97 ++++++++----------- .../com/bugsnag/CallbackSuppressionTest.java | 48 +++++++++ .../com/bugsnag/ConcurrentCallbackTest.java | 17 +--- .../test/java/com/bugsnag/ExceptionTest.java | 42 ++++---- .../bugsnag/JakartaServletCallbackTest.java | 6 +- .../src/test/java/com/bugsnag/MarkerTest.java | 8 +- .../java/com/bugsnag/SessionTrackerTest.java | 40 +++++++- .../example/logback/cli/Application.java | 8 +- .../bugsnag/example/simple/ExampleApp.java | 6 +- .../spring/web/ApplicationRestController.java | 3 +- .../bugsnag/example/spring/web/Config.java | 3 +- .../cli/ApplicationCommandLineRunner.java | 3 +- .../bugsnag/example/spring/cli/Config.java | 6 +- .../ScheduledTaskExecutorScenario.java | 3 +- .../scenarios/AutoRedactScenario.java | 3 +- .../scenarios/LogbackMetadataScenario.java | 15 +-- .../scenarios/ManualContextScenario.java | 3 +- .../scenarios/ManualRedactScenario.java | 3 +- .../scenarios/MetadataScenario.java | 3 +- .../scenarios/ThreadMetadataScenario.java | 6 +- .../UnhandledThreadMetadataScenario.java | 3 +- .../scenarios/UserCallbackScenario.java | 3 +- 39 files changed, 313 insertions(+), 214 deletions(-) delete mode 100644 bugsnag/src/main/java/com/bugsnag/BeforeSendSession.java create mode 100644 bugsnag/src/main/java/com/bugsnag/OnSession.java create mode 100644 bugsnag/src/test/java/com/bugsnag/CallbackSuppressionTest.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java index 36f70550..88aeae99 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java +++ b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java @@ -28,8 +28,9 @@ public class BugsnagSpringConfiguration implements InitializingBean { Callback springVersionErrorCallback() { Callback callback = new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { addSpringRuntimeVersion(report.getDevice()); + return true; } }; bugsnag.addCallback(callback); @@ -37,15 +38,16 @@ public void beforeNotify(Report report) { } @Bean - BeforeSendSession springVersionSessionCallback() { - BeforeSendSession beforeSendSession = new BeforeSendSession() { + OnSession springVersionSessionCallback() { + OnSession onSession = new OnSession() { @Override - public void beforeSendSession(SessionPayload payload) { + public boolean onSession(SessionPayload payload) { addSpringRuntimeVersion(payload.getDevice()); + return true; } }; - bugsnag.addBeforeSendSession(beforeSendSession); - return beforeSendSession; + bugsnag.addOnSession(onSession); + return onSession; } private void addSpringRuntimeVersion(Map device) { diff --git a/bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java b/bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java index 3c93b35b..008f09d1 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java +++ b/bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java @@ -111,7 +111,7 @@ class ExceptionClassCallback implements Callback { } @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { HandledState handledState = report.getHandledState(); @@ -119,7 +119,7 @@ public void beforeNotify(Report report) { SeverityReasonType severityReasonType = handledState.calculateSeverityReasonType(); if (severityReasonType == SeverityReasonType.REASON_USER_SPECIFIED || severityReasonType == SeverityReasonType.REASON_CALLBACK_SPECIFIED) { - return; + return true; // do not change delivery decision } Class exceptionClass = report.getException().getClass(); @@ -133,5 +133,6 @@ public void beforeNotify(Report report) { severity, handledState.isUnhandled())); } + return true; } } diff --git a/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java index f8f71ab0..aa5509ec 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java +++ b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java @@ -19,8 +19,9 @@ public class SpringBootConfiguration { Callback springBootVersionErrorCallback() { Callback callback = new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { addSpringRuntimeVersion(report.getDevice()); + return true; } }; bugsnag.addCallback(callback); @@ -28,15 +29,16 @@ public void beforeNotify(Report report) { } @Bean - BeforeSendSession springBootVersionSessionCallback() { - BeforeSendSession beforeSendSession = new BeforeSendSession() { + OnSession springBootVersionSessionCallback() { + OnSession onSession = new OnSession() { @Override - public void beforeSendSession(SessionPayload payload) { + public boolean onSession(SessionPayload payload) { addSpringRuntimeVersion(payload.getDevice()); + return true; } }; - bugsnag.addBeforeSendSession(beforeSendSession); - return beforeSendSession; + bugsnag.addOnSession(onSession); + return onSession; } private void addSpringRuntimeVersion(Map device) { diff --git a/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java index 6acb31a1..fcded466 100644 --- a/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java +++ b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java @@ -190,8 +190,9 @@ public void unhandledTypeMismatchExceptionCallbackSeverity() Report report; Callback callback = new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setSeverity(Severity.WARNING); + return true; } }; diff --git a/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestController.java b/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestController.java index 86e064bf..650bbf98 100644 --- a/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestController.java +++ b/bugsnag-spring/src/test/java/com/bugsnag/testapp/springboot/TestController.java @@ -67,8 +67,9 @@ public void handledTypeMismatchExceptionCallbackSeverity() { } catch (TypeMismatchException ex) { bugsnag.notify(ex, new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setSeverity(Severity.WARNING); + return true; } }); } diff --git a/bugsnag/src/main/java/com/bugsnag/BeforeSendSession.java b/bugsnag/src/main/java/com/bugsnag/BeforeSendSession.java deleted file mode 100644 index dcd6a50d..00000000 --- a/bugsnag/src/main/java/com/bugsnag/BeforeSendSession.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.bugsnag; - -interface BeforeSendSession { - void beforeSendSession(SessionPayload payload); -} diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 635a0439..21758962 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -452,16 +452,12 @@ public boolean notify(Report report, Callback reportCallback) { return false; } - // Run all client-wide beforeNotify callbacks + // Run all client-wide onError callbacks for (Callback callback : config.callbacks) { try { - // Run the callback - callback.beforeNotify(report); - - // Check if callback cancelled delivery - if (report.getShouldCancel()) { - LOGGER.debug("Error not reported to Bugsnag - " - + "cancelled by a client-wide beforeNotify callback"); + boolean proceed = callback.onError(report); + if (!proceed || report.getShouldCancel()) { + LOGGER.debug("Error not reported to Bugsnag - cancelled by a client-wide onError callback"); return false; } } catch (Throwable ex) { @@ -472,16 +468,12 @@ public boolean notify(Report report, Callback reportCallback) { // Add thread metadata to the report report.mergeMetadata(THREAD_METADATA.get()); - // Run the report-specific beforeNotify callback, if given + // Run the report-specific onError callback, if given if (reportCallback != null) { try { - // Run the callback - reportCallback.beforeNotify(report); - - // Check if callback cancelled delivery - if (report.getShouldCancel()) { - LOGGER.debug( - "Error not reported to Bugsnag - cancelled by a report-specific callback"); + boolean proceed = reportCallback.onError(report); + if (!proceed || report.getShouldCancel()) { + LOGGER.debug("Error not reported to Bugsnag - cancelled by a report-specific callback"); return false; } } catch (Throwable ex) { @@ -676,7 +668,7 @@ public static Set uncaughtExceptionClients() { return Collections.emptySet(); } - void addBeforeSendSession(BeforeSendSession beforeSendSession) { - sessionTracker.addBeforeSendSession(beforeSendSession); + void addOnSession(OnSession onSession) { + sessionTracker.addOnSession(onSession); } } diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index c65df317..7fe93bfd 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -134,7 +134,7 @@ protected void append(final ILoggingEvent event) { calculateSeverity(event), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { // Add some data from the logging event report.addToTab("Log event data", @@ -146,8 +146,12 @@ public void beforeNotify(Report report) { populateContextData(report, event); if (reportCallback != null) { - reportCallback.beforeNotify(report); + boolean proceed = reportCallback.onError(report); + if (!proceed) { + return false; // suppress delivery + } } + return true; } }); } @@ -270,7 +274,7 @@ private Bugsnag createBugsnag() { // Add a callback to put global metadata on every report bugsnag.addCallback(new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { for (LogbackMetadata metadata : globalMetadata) { for (LogbackMetadataTab tab : metadata.getTabs()) { @@ -282,6 +286,7 @@ public void beforeNotify(Report report) { } } + return true; } }); diff --git a/bugsnag/src/main/java/com/bugsnag/OnSession.java b/bugsnag/src/main/java/com/bugsnag/OnSession.java new file mode 100644 index 00000000..b3349f54 --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/OnSession.java @@ -0,0 +1,6 @@ +package com.bugsnag; + +@FunctionalInterface +interface OnSession { + boolean onSession(SessionPayload payload); +} diff --git a/bugsnag/src/main/java/com/bugsnag/SessionPayload.java b/bugsnag/src/main/java/com/bugsnag/SessionPayload.java index ac259dab..f7ef93d9 100644 --- a/bugsnag/src/main/java/com/bugsnag/SessionPayload.java +++ b/bugsnag/src/main/java/com/bugsnag/SessionPayload.java @@ -9,10 +9,21 @@ final class SessionPayload { private final Collection sessionCounts; private final Diagnostics diagnostics; + private final Map device; + private final Map app; SessionPayload(Collection sessionCounts, Configuration configuration) { this.sessionCounts = sessionCounts; diagnostics = new Diagnostics(configuration); + this.device = null; + this.app = null; + } + + SessionPayload(Collection sessionCounts, Map device, Map app) { + this.sessionCounts = sessionCounts; + this.diagnostics = null; + this.device = device; + this.app = app; } @Expose @@ -22,12 +33,12 @@ Notifier getNotifier() { @Expose Map getDevice() { - return diagnostics.device; + return device != null ? device : diagnostics.device; } @Expose Map getApp() { - return diagnostics.app; + return app != null ? app : diagnostics.app; } @Expose diff --git a/bugsnag/src/main/java/com/bugsnag/SessionTracker.java b/bugsnag/src/main/java/com/bugsnag/SessionTracker.java index d0a50472..65ab82a6 100644 --- a/bugsnag/src/main/java/com/bugsnag/SessionTracker.java +++ b/bugsnag/src/main/java/com/bugsnag/SessionTracker.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; @@ -16,12 +17,11 @@ class SessionTracker { private final Configuration config; private final ThreadLocal session = new ThreadLocal(); private final AtomicReference batchCount = new AtomicReference(); - private final Collection - enqueuedSessionCounts = new ConcurrentLinkedQueue(); + private final Collection enqueuedSessionCounts = new ConcurrentLinkedQueue(); private final Semaphore flushingRequest = new Semaphore(1); private final AtomicBoolean shuttingDown = new AtomicBoolean(); - private final Collection sessionCallbacks = new ConcurrentLinkedQueue(); + private final Collection sessionCallbacks = new ConcurrentLinkedQueue(); SessionTracker(Configuration configuration) { this.config = configuration; @@ -86,17 +86,40 @@ private void sendSessions(Date now) { if (!enqueuedSessionCounts.isEmpty() && flushingRequest.tryAcquire(1)) { try { - Collection requestValues - = new ArrayList(enqueuedSessionCounts); - SessionPayload payload = new SessionPayload(requestValues, config); + Collection requestValues = new ArrayList(enqueuedSessionCounts); + Collection approvedSessions = new ArrayList(); + SessionPayload firstPayload = null; + + for (SessionCount sessionCount : requestValues) { + SessionPayload payload = new SessionPayload(Collections.singleton(sessionCount), config); + + boolean sendThisSession = true; + for (OnSession callback : sessionCallbacks) { + if (!callback.onSession(payload)) { + sendThisSession = false; + break; + } + } - for (BeforeSendSession callback : sessionCallbacks) { - callback.beforeSendSession(payload); + if (sendThisSession) { + approvedSessions.add(sessionCount); + if (firstPayload == null) { + firstPayload = payload; + } + } + } + + if (!approvedSessions.isEmpty()) { + // Reuse the device/app from the first approved payload to preserve runtime + // versions + SessionPayload batchPayload = new SessionPayload(approvedSessions, firstPayload.getDevice(), + firstPayload.getApp()); + Delivery delivery = config.sessionDelivery; + delivery.deliver(config.serializer, batchPayload, config.getSessionApiHeaders()); } - Delivery delivery = config.sessionDelivery; - delivery.deliver(config.serializer, payload, config.getSessionApiHeaders()); enqueuedSessionCounts.removeAll(requestValues); + } finally { flushingRequest.release(1); } @@ -109,7 +132,7 @@ void shutdown() { } } - void addBeforeSendSession(BeforeSendSession beforeSendSession) { - sessionCallbacks.add(beforeSendSession); + void addOnSession(OnSession onSession) { + sessionCallbacks.add(onSession); } } diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java index 1b3db326..bcdc3b3b 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java @@ -11,7 +11,7 @@ public AppCallback(Configuration config) { } @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { if (config.appType != null) { report.setAppInfo("type", config.appType); } @@ -23,5 +23,6 @@ public void beforeNotify(Report report) { if (config.releaseStage != null) { report.setAppInfo("releaseStage", config.releaseStage); } + return true; } } diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/Callback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/Callback.java index 8ba80297..1ef45428 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/Callback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/Callback.java @@ -5,8 +5,12 @@ public interface Callback { /** * Perform changes to the report before delivery. + * Return {@code true} to continue sending, or {@code false} to cancel delivery. + * Implementations may also call {@code report.cancel()} for backward + * compatibility. * * @param report the report to perform changes on. + * @return true to send, false to suppress delivery */ - void beforeNotify(Report report); + boolean onError(Report report); } diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java index e6d21297..59005e59 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java @@ -84,12 +84,13 @@ public void run() { } @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report .addToTab("device", "osArch", System.getProperty("os.arch")) .addToTab("device", "locale", Locale.getDefault()) .setDeviceInfo("hostname", getHostnameValue()) .setDeviceInfo("osName", System.getProperty("os.name")) .setDeviceInfo("osVersion", System.getProperty("os.version")); + return true; } } diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java index 17c4f67c..b11dd73b 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java @@ -26,11 +26,11 @@ public static boolean isAvailable() { } @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { // Check if we have any servlet request data available HttpServletRequest request = BugsnagServletRequestListener.getServletRequest(); if (request == null) { - return; + return true; // nothing to add, but do not cancel } // Add request information to metadata @@ -46,6 +46,7 @@ public void beforeNotify(Report report) { if (report.getContext() == null) { report.setContext(request.getMethod() + " " + request.getRequestURI()); } + return true; } private String getClientIp(HttpServletRequest request) { diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java index 4ba4dc0a..682087c5 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java @@ -6,7 +6,6 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.logback.BugsnagMarker; @@ -109,14 +108,11 @@ public void testMetadataRemoval() { Bugsnag.addThreadMetadata("thread", "some key", "some thread value"); // Send three test logs, the first one with report metadata added - LOGGER.warn(new BugsnagMarker(new Callback() { - @Override - public void beforeNotify(Report report) { - report.addToTab("report", "some key", "some report value"); - } + LOGGER.warn(new BugsnagMarker(report -> { + report.addToTab("report", "some key", "some report value"); + return true; }), "Test exception", new RuntimeException("test")); - LOGGER.warn("Test exception", new RuntimeException("test")); Bugsnag.clearThreadMetadata(); LOGGER.warn("Test exception", new RuntimeException("test")); diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index 248e50e1..e09d41af 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -5,7 +5,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.logback.ProxyConfiguration; @@ -296,19 +295,16 @@ public void testRedactedKeys() { public void testCallback() { // Setup a callback to set the user - appender.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.setUserName("User Name"); - report.setUserEmail("user@example.com"); - report.setUserId("12345"); + appender.addCallback(report -> { + report.setUserName("User Name"); + report.setUserEmail("user@example.com"); + report.setUserId("12345"); - report.setContext("the context"); + report.setContext("the context"); - report.setGroupingHash("the grouping hash"); - - report.setApiKey("newapikey"); - } + report.setGroupingHash("the grouping hash"); + report.setApiKey("newapikey"); + return true; }); // Send a log message diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index e075be43..10d19d49 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -6,7 +6,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.delivery.HttpDelivery; import com.bugsnag.delivery.OutputStreamDelivery; @@ -186,14 +185,12 @@ public void deliver(Serializer serializer, Object object, Map he public void close() { } }); - assertTrue(bugsnag.notify(new Throwable(), new Callback() { - @Override - public void beforeNotify(Report report) { - report.addToTab("firsttab", "testredact1", "secretpassword"); - report.addToTab("firsttab", "testredact2", "secretpassword"); - report.addToTab("firsttab", "testredact3", "secretpassword"); - report.addToTab("secondtab", "testredact1", "secretpassword"); - } + assertTrue(bugsnag.notify(new Throwable(), report -> { + report.addToTab("firsttab", "testredact1", "secretpassword"); + report.addToTab("firsttab", "testredact2", "secretpassword"); + report.addToTab("firsttab", "testredact3", "secretpassword"); + report.addToTab("secondtab", "testredact1", "secretpassword"); + return true; })); } @@ -221,17 +218,15 @@ public void close() { } }); - assertTrue(bugsnag.notify(new Throwable(), new Callback() { - @Override - public void beforeNotify(Report report) { - Map headers = new HashMap(); - headers.put("Authorization", "User:Password"); - headers.put("authorization", "User:Password"); - headers.put("Cookie", "123456ABCDEF"); - headers.put("cookie", "123456ABCDEF"); + assertTrue(bugsnag.notify(new Throwable(), report -> { + Map headers = new HashMap(); + headers.put("Authorization", "User:Password"); + headers.put("authorization", "User:Password"); + headers.put("Cookie", "123456ABCDEF"); + headers.put("cookie", "123456ABCDEF"); - report.addToTab("request", "headers", headers); - } + report.addToTab("request", "headers", headers); + return true; })); } @@ -250,21 +245,17 @@ public void deliver(Serializer serializer, Object object, Map he public void close() { } }); - assertTrue(bugsnag.notify(new Throwable(), new Callback() { - @Override - public void beforeNotify(Report report) { - report.setUser("123", "test@example.com", "test name"); - } + assertTrue(bugsnag.notify(new Throwable(), report -> { + report.setUser("123", "test@example.com", "test name"); + return true; })); } @Test public void testContext() { - bugsnag.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.setContext("the context"); - } + bugsnag.addCallback(report -> { + report.setContext("the context"); + return true; }); bugsnag.setDelivery(new Delivery() { @Override @@ -282,11 +273,9 @@ public void close() { @Test public void testGroupingHash() { - bugsnag.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.setGroupingHash("the grouping hash"); - } + bugsnag.addCallback(report -> { + report.setGroupingHash("the grouping hash"); + return true; }); bugsnag.setDelivery(new Delivery() { @Override @@ -304,11 +293,9 @@ public void close() { @Test public void testSingleCallback() { - bugsnag.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.setApiKey("newapikey"); - } + bugsnag.addCallback(report -> { + report.setApiKey("newapikey"); + return true; }); bugsnag.setDelivery(new Delivery() { @Override @@ -338,27 +325,21 @@ public void close() { } }); - assertTrue(bugsnag.notify(new Throwable(), new Callback() { - @Override - public void beforeNotify(Report report) { - report.setApiKey("newapikey"); - } + assertTrue(bugsnag.notify(new Throwable(), report -> { + report.setApiKey("newapikey"); + return true; })); } @Test public void testCallbackOrder() { - bugsnag.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.setApiKey("newapikey"); - } + bugsnag.addCallback(report -> { + report.setApiKey("newapikey"); + return true; }); - bugsnag.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.setApiKey("secondnewapikey"); - } + bugsnag.addCallback(report -> { + report.setApiKey("secondnewapikey"); + return true; }); bugsnag.setDelivery(new Delivery() { @Override @@ -377,11 +358,9 @@ public void close() { @Test public void testCallbackCancel() { bugsnag.setDelivery(BugsnagTestUtils.generateDelivery()); - bugsnag.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - report.cancel(); - } + bugsnag.addCallback(report -> { + report.cancel(); + return true; // cancellation flag respected }); // Test the report is not sent assertFalse(bugsnag.notify(new Throwable())); diff --git a/bugsnag/src/test/java/com/bugsnag/CallbackSuppressionTest.java b/bugsnag/src/test/java/com/bugsnag/CallbackSuppressionTest.java new file mode 100644 index 00000000..3de59d54 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/CallbackSuppressionTest.java @@ -0,0 +1,48 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CallbackSuppressionTest { + + private Bugsnag bugsnag; + private StubNotificationDelivery delivery; + + /** + * Set up test fixtures. + */ + @Before + public void setUp() { + bugsnag = new Bugsnag("apikey"); + delivery = new StubNotificationDelivery(); + bugsnag.setDelivery(delivery); + } + + @After + public void tearDown() { + bugsnag.close(); + } + + @Test + public void callbackReturningFalseSuppressesDelivery() { + bugsnag.addCallback(report -> false); // explicit suppression + + boolean result = bugsnag.notify(new RuntimeException("Suppressed")); + assertFalse("notify should return false when suppressed", result); + assertEquals("No notifications should be delivered", 0, delivery.getNotifications().size()); + } + + @Test + public void callbackReturningTrueAllowsDelivery() { + bugsnag.addCallback(report -> true); // allow + + boolean result = bugsnag.notify(new RuntimeException("Allowed")); + assertTrue(result); + assertEquals(1, delivery.getNotifications().size()); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/ConcurrentCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/ConcurrentCallbackTest.java index dfbe2d31..ce6f8f50 100644 --- a/bugsnag/src/test/java/com/bugsnag/ConcurrentCallbackTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ConcurrentCallbackTest.java @@ -1,7 +1,5 @@ package com.bugsnag; -import com.bugsnag.callbacks.Callback; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -28,16 +26,11 @@ public void closeBugsnag() { public void testClientNotifyModification() { final Configuration config = bugsnag.getConfig(); - config.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - // modify the callback collection, when iterating to the next callback this should not crash - config.addCallback(new Callback() { - @Override - public void beforeNotify(Report report) { - } - }); - } + config.addCallback(report -> { + // modify the callback collection, when iterating to the next callback this + // should not crash + config.addCallback(r -> true); + return true; }); bugsnag.notify(new RuntimeException()); } diff --git a/bugsnag/src/test/java/com/bugsnag/ExceptionTest.java b/bugsnag/src/test/java/com/bugsnag/ExceptionTest.java index 861e1e36..918e04e9 100644 --- a/bugsnag/src/test/java/com/bugsnag/ExceptionTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ExceptionTest.java @@ -5,7 +5,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.serialization.Serializer; @@ -60,29 +59,26 @@ public void deliver(Serializer serializer, Object object, Map he public void close() { } }); - assertTrue(bugsnag.notify(ogThrowable, new Callback() { - @Override - public void beforeNotify(Report report) { - try { - assertEquals(ogThrowable, report.getException()); - assertEquals("Test", report.getExceptionMessage()); - assertEquals("java.lang.RuntimeException", report.getExceptionName()); - - report.setExceptionName("Foo"); - assertEquals("Foo", report.getExceptionName()); - - - List exceptions = report.getExceptions(); - assertEquals(1, exceptions.size()); - - Exception exception = exceptions.get(0); - assertNotNull(exception); - assertEquals("Foo", exception.getErrorClass()); - assertEquals("Test", exception.getMessage()); - } catch (Throwable throwable) { - report.cancel(); - } + assertTrue(bugsnag.notify(ogThrowable, report -> { + try { + assertEquals(ogThrowable, report.getException()); + assertEquals("Test", report.getExceptionMessage()); + assertEquals("java.lang.RuntimeException", report.getExceptionName()); + + report.setExceptionName("Foo"); + assertEquals("Foo", report.getExceptionName()); + + List exceptions = report.getExceptions(); + assertEquals(1, exceptions.size()); + + Exception exception = exceptions.get(0); + assertNotNull(exception); + assertEquals("Foo", exception.getErrorClass()); + assertEquals("Test", exception.getMessage()); + } catch (Throwable throwable) { + report.cancel(); } + return !report.getShouldCancel(); })); bugsnag.close(); diff --git a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java index 00ad7997..f71d0a61 100644 --- a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java +++ b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java @@ -83,7 +83,7 @@ public void closeBugsnag() { public void testRequestMetadataAdded() { Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); JakartaServletCallback callback = new JakartaServletCallback(); - callback.beforeNotify(report); + callback.onError(report); Map metadata = report.getMetadata(); assertTrue(metadata.containsKey("request")); @@ -120,7 +120,7 @@ public void testRequestMetadataAdded() { public void testRequestContextSet() { Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); JakartaServletCallback callback = new JakartaServletCallback(); - callback.beforeNotify(report); + callback.onError(report); assertEquals("PATCH /foo/bar", report.getContext()); } @@ -130,7 +130,7 @@ public void testExistingContextNotOverridden() { Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); report.setContext("Honey nut corn flakes"); JakartaServletCallback callback = new JakartaServletCallback(); - callback.beforeNotify(report); + callback.onError(report); assertEquals("Honey nut corn flakes", report.getContext()); } diff --git a/bugsnag/src/test/java/com/bugsnag/MarkerTest.java b/bugsnag/src/test/java/com/bugsnag/MarkerTest.java index f746cedd..69f62666 100644 --- a/bugsnag/src/test/java/com/bugsnag/MarkerTest.java +++ b/bugsnag/src/test/java/com/bugsnag/MarkerTest.java @@ -13,7 +13,6 @@ import java.util.Iterator; - /** * Tests for the Bugsnag Marker internal logic */ @@ -27,12 +26,7 @@ public class MarkerTest { */ @Before public void createMarker() { - callback = new Callback() { - @Override - public void beforeNotify(Report report) { - - } - }; + callback = report -> true; marker = new BugsnagMarker(callback); } diff --git a/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java b/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java index d1dcf9ec..872b0956 100644 --- a/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java +++ b/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java @@ -21,7 +21,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; - public class SessionTrackerTest { private SessionTracker sessionTracker; @@ -185,7 +184,6 @@ public void deliver(Serializer serializer, Object object, Map he super.deliver(serializer, object, headers); SessionPayload payload = (SessionPayload) object; - List sessionCounts = (List) payload.getSessionCounts(); assertEquals(3, sessionCounts.size()); @@ -284,7 +282,8 @@ public void deliver(Serializer serializer, Object object, Map he @Test public void zeroSessionCount() { - CustomDelivery sessionDelivery = new CustomDelivery() {}; + CustomDelivery sessionDelivery = new CustomDelivery() { + }; configuration.sessionDelivery = sessionDelivery; sessionTracker.flushSessions(new Date(10120000L)); sessionTracker.flushSessions(new Date(14000000L)); @@ -300,7 +299,8 @@ public void testSessionShutdownStartSession() { @Test public void testSessionShutdownDelivers() { - CustomDelivery delivery = new CustomDelivery() {}; + CustomDelivery delivery = new CustomDelivery() { + }; configuration.sessionDelivery = delivery; sessionTracker.startSession(new Date(), true); @@ -311,7 +311,8 @@ public void testSessionShutdownDelivers() { @Test public void testMultiShutdown() { - CustomDelivery delivery = new CustomDelivery() {}; + CustomDelivery delivery = new CustomDelivery() { + }; configuration.sessionDelivery = delivery; sessionTracker.startSession(new Date(), true); @@ -321,6 +322,35 @@ public void testMultiShutdown() { assertEquals(1, delivery.count.get()); } + @Test + public void sessionDeliverySuppressedByCallback() { + // Set up a delivery stub which SHOULD NOT be invoked + CustomDelivery delivery = new CustomDelivery() { + @Override + public void deliver(Serializer serializer, Object object, Map headers) { + super.deliver(serializer, object, headers); + fail("Delivery should be suppressed by OnSession callback returning false"); + } + }; + configuration.sessionDelivery = delivery; + + // Add callback which returns false to suppress sending + sessionTracker.addOnSession(new OnSession() { + @Override + public boolean onSession(SessionPayload payload) { + return false; // suppress delivery + } + }); + + // Start a session and flush far enough in future to trigger send attempt + sessionTracker.startSession(new Date(10000000L), false); + sessionTracker.flushSessions(new Date(13600000L)); // different batch period + + // Verify delivery was NOT performed + assertFalse(delivery.delivered); + assertEquals(0, delivery.count.get()); + } + abstract static class CustomDelivery implements Delivery { boolean delivered; AtomicInteger count = new AtomicInteger(0); diff --git a/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java b/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java index 08b77577..a15208c1 100644 --- a/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java +++ b/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java @@ -16,7 +16,8 @@ public class Application { public static void main(String[] args) throws Exception { - ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory + .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); Appender appender = rootLogger.getAppender("BUGSNAG"); if (appender instanceof BugsnagAppender) { // Set some global meta data (added to each report) @@ -28,6 +29,7 @@ public static void main(String[] args) throws Exception { report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); + return true; }); } @@ -58,6 +60,7 @@ public static void main(String[] args) throws Exception { LOGGER.warn(new BugsnagMarker((report) -> { report.addToTab("report tab", "data key 1", "data value 1"); report.addToTab("report tab", "data key 2", "data value 2"); + return true; }), "Something bad happened", e); } @@ -76,7 +79,8 @@ public void run() { // Wait for unhandled exception thread to finish before exiting thread.join(); - // Remove the thread metadata so it won't be added to future reports on this thread + // Remove the thread metadata so it won't be added to future reports on this + // thread Bugsnag.clearThreadMetadata(); // Exit the application diff --git a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java index cfb24441..41059c11 100644 --- a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java +++ b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java @@ -22,7 +22,7 @@ public static void main(String[] args) throws InterruptedException { // the lifecyle of your application bugsnag.addCallback(new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("diagnostics", "timestamp", new Date()); report.addToTab("customer", "name", "acme-inc"); report.addToTab("customer", "paying", true); @@ -30,6 +30,7 @@ public void beforeNotify(Report report) { report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); + return true; } }); @@ -53,10 +54,11 @@ public void beforeNotify(Report report) { } catch (RuntimeException e) { bugsnag.notify(e, new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setSeverity(Severity.WARNING); report.addToTab("report", "something", "that happened"); report.setContext("the context"); + return true; } }); } diff --git a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java index df5a5796..584934d8 100644 --- a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java +++ b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java @@ -71,10 +71,11 @@ public String sendHandledExceptionWithMetadata() { } catch (RuntimeException e) { bugsnag.notify(e, new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setSeverity(Severity.WARNING); report.addToTab("report", "something", "that happened"); report.setContext("the context"); + return true; } }); } diff --git a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java index 0481f59f..e7385514 100644 --- a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java +++ b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java @@ -31,7 +31,7 @@ public Bugsnag bugsnag() { // the lifecyle of your application bugsnag.addCallback(new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("diagnostics", "timestamp", new Date()); report.addToTab("customer", "name", "acme-inc"); report.addToTab("customer", "paying", true); @@ -39,6 +39,7 @@ public void beforeNotify(Report report) { report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); + return true; } }); diff --git a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java index 6e201859..2f26233d 100644 --- a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java +++ b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java @@ -50,10 +50,11 @@ public void run(final String... args) throws Exception { } catch (RuntimeException e) { bugsnag.notify(e, new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setSeverity(Severity.WARNING); report.addToTab("report", "something", "that happened"); report.setContext("the context"); + return true; } }); } diff --git a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java index 7921655a..3aff0375 100644 --- a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java +++ b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java @@ -15,7 +15,8 @@ @Import(BugsnagSpringConfiguration.class) public class Config { - // Define singleton bean "bugsnag" which can be injected into any Spring managed class with @Autowired. + // Define singleton bean "bugsnag" which can be injected into any Spring managed + // class with @Autowired. @Bean public Bugsnag bugsnag() { // Create a Bugsnag client @@ -31,7 +32,7 @@ public Bugsnag bugsnag() { // the lifecyle of your application bugsnag.addCallback(new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("diagnostics", "timestamp", new Date()); report.addToTab("customer", "name", "acme-inc"); report.addToTab("customer", "paying", true); @@ -39,6 +40,7 @@ public void beforeNotify(Report report) { report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); + return true; } }); diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java index 5dc877b2..3ab87cd8 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java @@ -27,9 +27,10 @@ public void run() { final Collection threadnames = ScheduledTaskExecutorService.getThreadNames(); bugsnag.notify(new RuntimeException("Whoops"), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("executor", "multiThreaded", threadnames.size() > 1); report.addToTab("executor", "names", threadnames); + return true; } }); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java index bfafab71..3615278c 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java @@ -17,10 +17,11 @@ public AutoRedactScenario(Bugsnag bugsnag) { public void run() { bugsnag.notify(generateException(), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("user", "password", "hunter2"); report.addToTab("custom", "password", "hunter2"); report.addToTab("custom", "foo", "hunter2"); + return true; } }); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java index 37bd1ad7..4f19a002 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java @@ -22,12 +22,13 @@ public LogbackMetadataScenario(Bugsnag bugsnag) { @Override public void run() { LOGGER.warn(new BugsnagMarker(new Callback() { - @Override - public void beforeNotify(Report report) { - report.addToTab("user", "foo", "hunter2"); - report.addToTab("custom", "foo", "hunter2"); - report.addToTab("custom", "bar", "hunter2"); - } - }),"Error sent to Bugsnag using the logback appender", generateException()); + @Override + public boolean onError(Report report) { + report.addToTab("user", "foo", "hunter2"); + report.addToTab("custom", "foo", "hunter2"); + report.addToTab("custom", "bar", "hunter2"); + return true; + } + }), "Error sent to Bugsnag using the logback appender", generateException()); } } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualContextScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualContextScenario.java index 2f518812..b25b8425 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualContextScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualContextScenario.java @@ -17,8 +17,9 @@ public ManualContextScenario(Bugsnag bugsnag) { public void run() { bugsnag.notify(generateException(), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setContext("FooContext"); + return true; } }); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java index 22685510..b518d3dd 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java @@ -20,10 +20,11 @@ public void run() { bugsnag.notify(generateException(), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("user", "foo", "hunter2"); report.addToTab("custom", "foo", "hunter2"); report.addToTab("custom", "bar", "hunter2"); + return true; } }); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java index e639c540..afe44e3e 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java @@ -17,8 +17,9 @@ public MetadataScenario(Bugsnag bugsnag) { public void run() { bugsnag.notify(generateException(), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("Custom", "foo", "Hello World!"); + return true; } }); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java index 254718fa..81252d8c 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java @@ -18,9 +18,10 @@ public void run() { // Global callback metadata has lowest precedence bugsnag.addCallback(new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("Custom", "test", "Global value"); report.addToTab("Custom", "foo", "Global value to be overwritten"); + return true; } }); @@ -47,8 +48,9 @@ public void run() { // Report-specific metadata should merge with global + thread metadata and overwrite when duplicate key bugsnag.notify(generateException(), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("Custom", "bar", "Hello World!"); + return true; } }); } diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java index c8c3dbba..f4b9afe4 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java @@ -18,9 +18,10 @@ public void run() { // Global callback metadata has lowest precedence bugsnag.addCallback(new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.addToTab("Custom", "test", "Global value"); report.addToTab("Custom", "foo", "Global value to be overwritten"); + return true; } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UserCallbackScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UserCallbackScenario.java index 52289a2b..61ad3ce7 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UserCallbackScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UserCallbackScenario.java @@ -17,8 +17,9 @@ public UserCallbackScenario(Bugsnag bugsnag) { public void run() { bugsnag.notify(generateException(), new Callback() { @Override - public void beforeNotify(Report report) { + public boolean onError(Report report) { report.setUser("Agent Pink", "bob@example.com", "Zebedee"); + return true; } }); } From 3b69aef294859278b212e794231127b2798066ec Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Mon, 24 Nov 2025 10:32:27 +0100 Subject: [PATCH 08/10] Rename endpointConfiguration to endpoints (#242) * rename * checkstyle --- bugsnag/src/main/java/com/bugsnag/Configuration.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index d7652574..1f364816 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -35,7 +35,7 @@ public class Configuration { public String appType; public String appVersion; public Delivery delivery; - public EndpointConfiguration endpointConfiguration; + public EndpointConfiguration endpoints; public Delivery sessionDelivery; public String[] redactedKeys = new String[] {"password", "secret", "Authorization", "Cookie"}; public String[] discardClasses; @@ -56,10 +56,10 @@ public class Configuration { addCallback(new DeviceCallback()); DeviceCallback.initializeCache(); - endpointConfiguration = EndpointConfiguration.fromApiKey(apiKey); + endpoints = EndpointConfiguration.fromApiKey(apiKey); - this.delivery = new AsyncHttpDelivery(endpointConfiguration.getNotifyEndpoint()); - this.sessionDelivery = new AsyncHttpDelivery(endpointConfiguration.getSessionEndpoint()); + this.delivery = new AsyncHttpDelivery(endpoints.getNotifyEndpoint()); + this.sessionDelivery = new AsyncHttpDelivery(endpoints.getSessionEndpoint()); if (JakartaServletCallback.isAvailable()) { addCallback(new JakartaServletCallback()); From c1a713c7d36f263a8062aef9738e3ae3c34e381d Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Mon, 1 Dec 2025 13:49:33 +0100 Subject: [PATCH 09/10] Encapsulation (#243) * initial change * checkstyle * checkstyle * make config access safer --- .../src/main/java/com/bugsnag/Bugsnag.java | 86 ++++++----- .../main/java/com/bugsnag/Configuration.java | 143 +++++++++++++++--- .../main/java/com/bugsnag/Diagnostics.java | 8 +- .../src/main/java/com/bugsnag/Metadata.java | 14 +- .../main/java/com/bugsnag/Notification.java | 2 +- bugsnag/src/main/java/com/bugsnag/Report.java | 16 +- .../main/java/com/bugsnag/SessionTracker.java | 4 +- .../com/bugsnag/callbacks/AppCallback.java | 12 +- .../test/java/com/bugsnag/AppenderTest.java | 46 +++--- .../java/com/bugsnag/ConfigurationTest.java | 22 +-- .../test/java/com/bugsnag/MetadataTest.java | 16 +- .../java/com/bugsnag/NotificationTest.java | 9 +- .../java/com/bugsnag/SessionPayloadTest.java | 6 +- .../java/com/bugsnag/SessionTrackerTest.java | 30 ++-- 14 files changed, 266 insertions(+), 148 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index 21758962..8914ac15 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -151,7 +151,7 @@ public void addCallback(Callback callback) { * @see Delivery */ public Delivery getDelivery() { - return config.delivery; + return config.getDelivery(); } /** @@ -161,17 +161,16 @@ public Delivery getDelivery() { * @see Delivery */ public Delivery getSessionDelivery() { - return config.sessionDelivery; + return config.getSessionDelivery(); } - /** * Set the application type sent to Bugsnag. * * @param appType the app type to send, eg. spring, gradleTask */ public void setAppType(String appType) { - config.appType = appType; + config.setAppType(appType); } /** @@ -180,7 +179,7 @@ public void setAppType(String appType) { * @param appVersion the app version to send */ public void setAppVersion(String appVersion) { - config.appVersion = appVersion; + config.setAppVersion(appVersion); } /** @@ -194,10 +193,9 @@ public void setAppVersion(String appVersion) { * @see Delivery */ public void setDelivery(Delivery delivery) { - config.delivery = delivery; + config.setDelivery(delivery); } - /** * Set the method of delivery for Bugsnag sessions. By default we'll * send sessions asynchronously using a thread pool to @@ -209,7 +207,7 @@ public void setDelivery(Delivery delivery) { * @see Delivery */ public void setSessionDelivery(Delivery delivery) { - config.sessionDelivery = delivery; + config.setSessionDelivery(delivery); } /** @@ -222,8 +220,9 @@ public void setSessionDelivery(Delivery delivery) { */ @Deprecated public void setEndpoint(String endpoint) { - if (config.delivery instanceof HttpDelivery) { - ((HttpDelivery) config.delivery).setEndpoint(endpoint); + Delivery delivery = config.getDelivery(); + if (delivery instanceof HttpDelivery) { + ((HttpDelivery) delivery).setEndpoint(endpoint); } } @@ -237,7 +236,7 @@ public void setEndpoint(String endpoint) { * @param redactedKeys a list of String keys to redact from metadata */ public void setRedactedKeys(String... redactedKeys) { - config.redactedKeys = redactedKeys; + config.setRedactedKeys(redactedKeys); } /** @@ -246,7 +245,7 @@ public void setRedactedKeys(String... redactedKeys) { * @param discardClasses a list of exception classes to ignore */ public void setDiscardClasses(String... discardClasses) { - config.discardClasses = discardClasses; + config.setDiscardClasses(discardClasses); } /** @@ -258,9 +257,9 @@ public void setDiscardClasses(String... discardClasses) { */ public void setEnabledReleaseStages(String... enabledReleaseStages) { if (enabledReleaseStages == null || enabledReleaseStages.length == 0) { - config.enabledReleaseStages = Collections.emptySet(); + config.setEnabledReleaseStages(Collections.emptySet()); } else { - config.enabledReleaseStages = Set.of(enabledReleaseStages); + config.setEnabledReleaseStages(Set.of(enabledReleaseStages)); } } @@ -271,21 +270,25 @@ public void setEnabledReleaseStages(String... enabledReleaseStages) { * @param projectPackages a list of package names */ public void setProjectPackages(String... projectPackages) { - config.projectPackages = projectPackages; + config.setProjectPackages(projectPackages); } /** - * Set a proxy to use when delivering Bugsnag error reports and sessions. This is a convenient + * Set a proxy to use when delivering Bugsnag error reports and sessions. This + * is a convenient * shorthand for bugsnag.getDelivery().setProxy(); * * @param proxy the proxy to use to send reports */ public void setProxy(Proxy proxy) { - if (config.delivery instanceof HttpDelivery) { - ((HttpDelivery) config.delivery).setProxy(proxy); + Delivery delivery = config.getDelivery(); + if (delivery instanceof HttpDelivery) { + ((HttpDelivery) delivery).setProxy(proxy); } - if (config.sessionDelivery instanceof HttpDelivery) { - ((HttpDelivery) config.sessionDelivery).setProxy(proxy); + + Delivery sessionDelivery = config.getSessionDelivery(); + if (sessionDelivery instanceof HttpDelivery) { + ((HttpDelivery) sessionDelivery).setProxy(proxy); } } @@ -296,7 +299,7 @@ public void setProxy(Proxy proxy) { * @see #setEnabledReleaseStages */ public void setReleaseStage(String releaseStage) { - config.releaseStage = releaseStage; + config.setReleaseStage(releaseStage); } /** @@ -309,7 +312,7 @@ public void setReleaseStage(String releaseStage) { * @see #setEnabledReleaseStages */ public void setSendThreads(boolean sendThreads) { - config.sendThreads = sendThreads; + config.setSendThreads(sendThreads); } /** @@ -321,15 +324,17 @@ public void setSendThreads(boolean sendThreads) { * @see #setDelivery */ public void setTimeout(int timeout) { - if (config.delivery instanceof HttpDelivery) { - ((HttpDelivery) config.delivery).setTimeout(timeout); + Delivery delivery = config.getDelivery(); + if (delivery instanceof HttpDelivery) { + ((HttpDelivery) delivery).setTimeout(timeout); } - if (config.sessionDelivery instanceof HttpDelivery) { - ((HttpDelivery) config.sessionDelivery).setTimeout(timeout); + + Delivery sessionDelivery = config.getSessionDelivery(); + if (sessionDelivery instanceof HttpDelivery) { + ((HttpDelivery) sessionDelivery).setTimeout(timeout); } } - // // Notification // @@ -416,7 +421,6 @@ public boolean notify(Report report) { return notify(report, null); } - boolean notify(Throwable throwable, HandledState handledState, Thread currentThread) { Report report = new Report(config, throwable, handledState, currentThread); return notify(report, null); @@ -448,7 +452,7 @@ public boolean notify(Report report, Callback reportCallback) { // Don't notify unless releaseStage is in enabledReleaseStages if (!config.shouldNotifyForReleaseStage()) { LOGGER.debug("Error not reported to Bugsnag - {} is not in 'enabledReleaseStages'", - config.releaseStage); + config.getReleaseStage()); return false; } @@ -481,7 +485,8 @@ public boolean notify(Report report, Callback reportCallback) { } } - if (config.delivery == null) { + Delivery delivery = config.getDelivery(); + if (delivery == null) { LOGGER.debug("Error not reported to Bugsnag - no delivery is set"); return false; } @@ -504,7 +509,7 @@ public boolean notify(Report report, Callback reportCallback) { // Deliver the notification LOGGER.debug("Reporting error to Bugsnag"); - config.delivery.deliver(config.serializer, notification, config.getErrorApiHeaders()); + delivery.deliver(config.getSerializer(), notification, config.getErrorApiHeaders()); return true; } @@ -556,8 +561,9 @@ public boolean shouldAutoCaptureSessions() { */ @Deprecated public void setSessionEndpoint(String endpoint) { - if (config.sessionDelivery instanceof HttpDelivery) { - ((HttpDelivery) config.sessionDelivery).setEndpoint(endpoint); + Delivery sessionDelivery = config.getSessionDelivery(); + if (sessionDelivery instanceof HttpDelivery) { + ((HttpDelivery) sessionDelivery).setEndpoint(endpoint); } } @@ -595,14 +601,16 @@ public void close() { LOGGER.debug("Closing connection to Bugsnag"); ExceptionHandler.disable(this); - // runs periodic checks, should shut down immediately as don't need to send any sessions + // runs periodic checks, should shut down immediately as don't need to send any + // sessions sessionExecutorService.shutdownNow(); // flush remaining sessions sessionTracker.shutdown(); - if (config.delivery != null) { - config.delivery.close(); + Delivery delivery = config.getDelivery(); + if (delivery != null) { + delivery.close(); } } @@ -616,7 +624,7 @@ public void close() { * @param value the metadata value to add */ public static void addThreadMetadata(String tabName, String key, Object value) { - THREAD_METADATA.get().addToTab(tabName, key, value); + THREAD_METADATA.get().addMetadata(tabName, key, value); } /** @@ -632,7 +640,7 @@ public static void clearThreadMetadata() { * @param tabName the name of the tab to remove */ public static void clearThreadMetadata(String tabName) { - THREAD_METADATA.get().clearTab(tabName); + THREAD_METADATA.get().clearMetadata(tabName); } /** @@ -642,7 +650,7 @@ public static void clearThreadMetadata(String tabName) { * @param key the key of the metadata to remove */ public static void clearThreadMetadata(String tabName, String key) { - THREAD_METADATA.get().clearKey(tabName, key); + THREAD_METADATA.get().clearMetadata(tabName, key); } Configuration getConfig() { diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index 1f364816..7fd5d536 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -31,19 +31,19 @@ public class Configuration { private static final String HEADER_API_KEY = "Bugsnag-Api-Key"; private static final String HEADER_BUGSNAG_SENT_AT = "Bugsnag-Sent-At"; - public String apiKey; - public String appType; - public String appVersion; - public Delivery delivery; - public EndpointConfiguration endpoints; - public Delivery sessionDelivery; - public String[] redactedKeys = new String[] {"password", "secret", "Authorization", "Cookie"}; - public String[] discardClasses; - public Set enabledReleaseStages = null; - public String[] projectPackages; - public String releaseStage; - public boolean sendThreads = false; - public Serializer serializer = new DefaultSerializer(); + private String apiKey; + private String appType; + private String appVersion; + private Delivery delivery; + private EndpointConfiguration endpoints; + private Delivery sessionDelivery; + private String[] redactedKeys = new String[] {"password", "secret", "Authorization", "Cookie"}; + private String[] discardClasses; + private Set enabledReleaseStages = null; + private String[] projectPackages; + private String releaseStage; + private boolean sendThreads = false; + private Serializer serializer = new DefaultSerializer(); Collection callbacks = new ConcurrentLinkedQueue(); private final AtomicBoolean autoCaptureSessions = new AtomicBoolean(true); @@ -128,15 +128,19 @@ public void setEndpoints(String notify, String sessions) throws IllegalArgumentE * Set the endpoints to send data to. Use this to override the default endpoints * if you are using Bugsnag Enterprise to point to your own Bugsnag endpoint. *

- * Please note that it is recommended that you set both endpoints. If the notify endpoint is - * missing, an exception will be thrown. If the session endpoint is missing, a warning will be + * Please note that it is recommended that you set both endpoints. If the notify + * endpoint is + * missing, an exception will be thrown. If the session endpoint is missing, a + * warning will be * logged and sessions will not be sent automatically. *

- * Note that if you are setting a custom {@link Delivery}, this method should be called after + * Note that if you are setting a custom {@link Delivery}, this method should be + * called after * the custom implementation has been set. * * @param endpointConfiguration the endpoint configuration - * @throws IllegalArgumentException if the endpoint configuration is null or if the notify endpoint is empty or null + * @throws IllegalArgumentException if the endpoint configuration is null or if + * the notify endpoint is empty or null */ public void setEndpoints(EndpointConfiguration endpointConfiguration) throws IllegalArgumentException { if (endpointConfiguration == null) { @@ -190,4 +194,109 @@ Map getSessionApiHeaders() { map.put(HEADER_BUGSNAG_SENT_AT, DateUtils.toIso8601(new Date())); return map; } + + // Accessors + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getAppType() { + return appType; + } + + public void setAppType(String appType) { + this.appType = appType; + } + + public String getAppVersion() { + return appVersion; + } + + public void setAppVersion(String appVersion) { + this.appVersion = appVersion; + } + + public Delivery getDelivery() { + return delivery; + } + + public void setDelivery(Delivery delivery) { + this.delivery = delivery; + } + + public EndpointConfiguration getEndpointsConfiguration() { + return endpoints; + } + + public void setEndpointsConfiguration(EndpointConfiguration endpoints) { + this.endpoints = endpoints; + } + + public Delivery getSessionDelivery() { + return sessionDelivery; + } + + public void setSessionDelivery(Delivery sessionDelivery) { + this.sessionDelivery = sessionDelivery; + } + + public String[] getRedactedKeys() { + return redactedKeys; + } + + public void setRedactedKeys(String[] redactedKeys) { + this.redactedKeys = redactedKeys; + } + + public String[] getDiscardClasses() { + return discardClasses; + } + + public void setDiscardClasses(String[] discardClasses) { + this.discardClasses = discardClasses; + } + + public Set getEnabledReleaseStages() { + return enabledReleaseStages; + } + + public void setEnabledReleaseStages(Set enabledReleaseStages) { + this.enabledReleaseStages = enabledReleaseStages; + } + + public String[] getProjectPackages() { + return projectPackages; + } + + public void setProjectPackages(String[] projectPackages) { + this.projectPackages = projectPackages; + } + + public String getReleaseStage() { + return releaseStage; + } + + public void setReleaseStage(String releaseStage) { + this.releaseStage = releaseStage; + } + + public boolean isSendThreads() { + return sendThreads; + } + + public void setSendThreads(boolean sendThreads) { + this.sendThreads = sendThreads; + } + + public Serializer getSerializer() { + return serializer; + } + + public void setSerializer(Serializer serializer) { + this.serializer = serializer; + } } diff --git a/bugsnag/src/main/java/com/bugsnag/Diagnostics.java b/bugsnag/src/main/java/com/bugsnag/Diagnostics.java index 671e3fed..6164e680 100644 --- a/bugsnag/src/main/java/com/bugsnag/Diagnostics.java +++ b/bugsnag/src/main/java/com/bugsnag/Diagnostics.java @@ -37,11 +37,11 @@ private Map getRuntimeVersions() { private Map getDefaultAppInfo(Configuration configuration) { Map map = new HashMap(); - if (configuration.releaseStage != null) { - map.put("releaseStage", configuration.releaseStage); + if (configuration.getReleaseStage() != null) { + map.put("releaseStage", configuration.getReleaseStage()); } - if (configuration.appVersion != null) { - map.put("version", configuration.appVersion); + if (configuration.getAppVersion() != null) { + map.put("version", configuration.getAppVersion()); } return map; } diff --git a/bugsnag/src/main/java/com/bugsnag/Metadata.java b/bugsnag/src/main/java/com/bugsnag/Metadata.java index 92fd4ea6..cd69958c 100644 --- a/bugsnag/src/main/java/com/bugsnag/Metadata.java +++ b/bugsnag/src/main/java/com/bugsnag/Metadata.java @@ -6,28 +6,28 @@ class Metadata extends HashMap { private static final long serialVersionUID = 2530038179702722770L; - public void addToTab(String tabName, String key, Object value) { - Map tab = getTab(tabName); + public void addMetadata(String tabName, String key, Object value) { + Map tab = getMetadata(tabName); tab.put(key, value); } - void clearTab(String tabName) { + void clearMetadata(String tabName) { remove(tabName); } - void clearKey(String tabName, String key) { - Map tab = getTab(tabName); + void clearMetadata(String tabName, String key) { + Map tab = getMetadata(tabName); tab.remove(key); } void merge(Metadata metadata) { for (String tabName : metadata.keySet()) { - getTab(tabName).putAll(metadata.getTab(tabName)); + getMetadata(tabName).putAll(metadata.getMetadata(tabName)); } } @SuppressWarnings(value = "unchecked") - private Map getTab(String tabName) { + private Map getMetadata(String tabName) { Map tab = (Map) get(tabName); if (tab == null) { tab = new HashMap(); diff --git a/bugsnag/src/main/java/com/bugsnag/Notification.java b/bugsnag/src/main/java/com/bugsnag/Notification.java index eed27ce6..62b4f0db 100644 --- a/bugsnag/src/main/java/com/bugsnag/Notification.java +++ b/bugsnag/src/main/java/com/bugsnag/Notification.java @@ -17,7 +17,7 @@ class Notification { @Expose public String getApiKey() { String reportApiKey = report.getApiKey(); - return reportApiKey != null ? reportApiKey : config.apiKey; + return reportApiKey != null ? reportApiKey : config.getApiKey(); } @Expose diff --git a/bugsnag/src/main/java/com/bugsnag/Report.java b/bugsnag/src/main/java/com/bugsnag/Report.java index 5f963baf..53f23630 100644 --- a/bugsnag/src/main/java/com/bugsnag/Report.java +++ b/bugsnag/src/main/java/com/bugsnag/Report.java @@ -38,14 +38,14 @@ protected Report(Configuration config, Throwable throwable) { } Report(Configuration config, Throwable throwable, - HandledState handledState, Thread currentThread) { + HandledState handledState, Thread currentThread) { this.config = config; this.exception = new Exception(config, throwable); this.handledState = handledState; this.severity = handledState.getOriginalSeverity(); diagnostics = new Diagnostics(this.config); - if (config.sendThreads) { + if (config.isSendThreads()) { Throwable exc = handledState.isUnhandled() ? throwable : null; Map allStackTraces = Thread.getAllStackTraces(); threadStates = ThreadState.getLiveThreads(config, currentThread, allStackTraces, exc); @@ -127,7 +127,7 @@ public Map getUser() { @Expose @JsonProperty("metaData") public Map getMetadata() { - return new RedactedMap(diagnostics.metadata, Set.of(config.redactedKeys)); + return new RedactedMap(diagnostics.metadata, Set.of(config.getRedactedKeys())); } @Expose @@ -151,7 +151,8 @@ void setSession(Session session) { } /** - * @return The {@linkplain Throwable exception} which triggered this error report. + * @return The {@linkplain Throwable exception} which triggered this error + * report. */ public Throwable getException() { return exception.getThrowable(); @@ -189,7 +190,7 @@ public String getExceptionMessage() { * @return the modified report */ public Report addToTab(String tabName, String key, Object value) { - diagnostics.metadata.addToTab(tabName, key, value); + diagnostics.metadata.addMetadata(tabName, key, value); return this; } @@ -200,7 +201,7 @@ public Report addToTab(String tabName, String key, Object value) { * @return The message from the exception contained in this error report. */ public Report clearTab(String tabName) { - diagnostics.metadata.clearTab(tabName); + diagnostics.metadata.clearMetadata(tabName); return this; } @@ -264,7 +265,8 @@ public Report setDeviceInfo(String key, Object value) { } /** - * Set the grouping hash on the report. Events will the same grouping hash will be grouped into + * Set the grouping hash on the report. Events will the same grouping hash will + * be grouped into * the same error in Bugsnag. For use if custom grouping is required. * * @param groupingHash the grouping hash for the error report diff --git a/bugsnag/src/main/java/com/bugsnag/SessionTracker.java b/bugsnag/src/main/java/com/bugsnag/SessionTracker.java index 65ab82a6..783dec71 100644 --- a/bugsnag/src/main/java/com/bugsnag/SessionTracker.java +++ b/bugsnag/src/main/java/com/bugsnag/SessionTracker.java @@ -114,8 +114,8 @@ private void sendSessions(Date now) { // versions SessionPayload batchPayload = new SessionPayload(approvedSessions, firstPayload.getDevice(), firstPayload.getApp()); - Delivery delivery = config.sessionDelivery; - delivery.deliver(config.serializer, batchPayload, config.getSessionApiHeaders()); + Delivery delivery = config.getSessionDelivery(); + delivery.deliver(config.getSerializer(), batchPayload, config.getSessionApiHeaders()); } enqueuedSessionCounts.removeAll(requestValues); diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java index bcdc3b3b..3efdf6ff 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/AppCallback.java @@ -12,16 +12,16 @@ public AppCallback(Configuration config) { @Override public boolean onError(Report report) { - if (config.appType != null) { - report.setAppInfo("type", config.appType); + if (config.getAppType() != null) { + report.setAppInfo("type", config.getAppType()); } - if (config.appVersion != null) { - report.setAppInfo("version", config.appVersion); + if (config.getAppVersion() != null) { + report.setAppInfo("version", config.getAppVersion()); } - if (config.releaseStage != null) { - report.setAppInfo("releaseStage", config.releaseStage); + if (config.getReleaseStage() != null) { + report.setAppInfo("releaseStage", config.getReleaseStage()); } return true; } diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java index e09d41af..e0e5756a 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderTest.java @@ -21,10 +21,10 @@ import java.util.List; import java.util.Map; - /** * Test for the Bugsnag Appender - * NOTE: Not called BugsnagAppenderTest because that throws away errors to prevent cycles + * NOTE: Not called BugsnagAppenderTest because that throws away errors to + * prevent cycles */ public class AppenderTest { @@ -44,9 +44,8 @@ public class AppenderTest { @Before public void swapDelivery() { - ch.qos.logback.classic.Logger rootLogger = - (ch.qos.logback.classic.Logger) LoggerFactory - .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory + .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); appender = (BugsnagAppender) rootLogger.getAppender("BUGSNAG"); Bugsnag bugsnag = appender.getClient(); @@ -138,31 +137,31 @@ public void testBugsnagConfig() { // Get the Bugsnag instance Configuration config = getConfig(appender.getClient()); - assertEquals("test", config.releaseStage); - assertEquals("1.0.1", config.appVersion); - assertEquals("gradleTask", config.appType); + assertEquals("test", config.getReleaseStage()); + assertEquals("1.0.1", config.getAppVersion()); + assertEquals("gradleTask", config.getAppType()); assertFalse(config.shouldAutoCaptureSessions()); - assertEquals(2, config.redactedKeys.length); - ArrayList redactedKeys = new ArrayList(Arrays.asList(config.redactedKeys)); + assertEquals(2, config.getRedactedKeys().length); + ArrayList redactedKeys = new ArrayList(Arrays.asList(config.getRedactedKeys())); assertTrue(redactedKeys.contains("password")); assertTrue(redactedKeys.contains("credit_card_number")); - assertEquals(2, config.discardClasses.length); - ArrayList discardClasses = new ArrayList(Arrays.asList(config.discardClasses)); + 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")); - assertEquals(2, config.enabledReleaseStages.size()); - assertTrue(config.enabledReleaseStages.contains("development")); - assertTrue(config.enabledReleaseStages.contains("test")); + assertEquals(2, config.getEnabledReleaseStages().size()); + assertTrue(config.getEnabledReleaseStages().contains("development")); + assertTrue(config.getEnabledReleaseStages().contains("test")); - assertEquals(2, config.projectPackages.length); - ArrayList projectPackages = new ArrayList(Arrays.asList(config.projectPackages)); + assertEquals(2, config.getProjectPackages().length); + ArrayList projectPackages = new ArrayList(Arrays.asList(config.getProjectPackages())); assertTrue(projectPackages.contains("com.company.package2")); assertTrue(projectPackages.contains("com.company.package1")); - assertTrue(config.sendThreads); + assertTrue(config.isSendThreads()); } @Test @@ -357,7 +356,7 @@ public void testSendThreads() { @Test public void testSplit() { assertTrue(appender.split(null).isEmpty()); - assertArrayEquals(new String[]{""}, appender.split("").toArray()); + assertArrayEquals(new String[] {""}, appender.split("").toArray()); String[] expected = {"one", "two", "three"}; assertArrayEquals(expected, appender.split("one,two,three").toArray()); @@ -367,13 +366,14 @@ public void testSplit() { public void testCreateFromExistingClient() { Bugsnag client = new Bugsnag("testApiKey"); BugsnagAppender appender = new BugsnagAppender(client); - // Make sure configuration changes are not passed through to the provided client. + // Make sure configuration changes are not passed through to the provided + // client. appender.setApiKey("newApiKey"); // Make sure a new client is not created when starting the appender. appender.start(); assertEquals(client, appender.getClient()); - assertEquals("testApiKey", appender.getClient().getConfig().apiKey); + assertEquals("testApiKey", appender.getClient().getConfig().getApiKey()); } private StackTraceElement changeClassName(StackTraceElement element, String className) { @@ -407,10 +407,10 @@ private SessionTracker getSessionTracker(Bugsnag bugsnag) { * Gets a hashmap key from the metadata in a notification * * @param notification The notification - * @param key The key to get + * @param key The key to get * @return The hash map */ - @SuppressWarnings (value = "unchecked") + @SuppressWarnings(value = "unchecked") private Map getMetadataMap(Notification notification, String key) { return ((Map) notification.getEvents().get(0).getMetadata().get(key)); } diff --git a/bugsnag/src/test/java/com/bugsnag/ConfigurationTest.java b/bugsnag/src/test/java/com/bugsnag/ConfigurationTest.java index f45d28bd..068c7567 100644 --- a/bugsnag/src/test/java/com/bugsnag/ConfigurationTest.java +++ b/bugsnag/src/test/java/com/bugsnag/ConfigurationTest.java @@ -38,8 +38,8 @@ public class ConfigurationTest { @Before public void setUp() { config = new Configuration("foo"); - config.delivery = new FakeHttpDelivery(); - config.sessionDelivery = new FakeHttpDelivery(); + config.setDelivery(new FakeHttpDelivery()); + config.setSessionDelivery(new FakeHttpDelivery()); } @Test @@ -50,7 +50,7 @@ public void testDefaults() { @Test public void testErrorApiHeaders() { Map headers = config.getErrorApiHeaders(); - assertEquals(config.apiKey, headers.get("Bugsnag-Api-Key")); + assertEquals(config.getApiKey(), headers.get("Bugsnag-Api-Key")); assertNotNull(headers.get("Bugsnag-Sent-At")); assertNotNull(headers.get("Bugsnag-Payload-Version")); } @@ -58,7 +58,7 @@ public void testErrorApiHeaders() { @Test public void testSessionApiHeaders() { Map headers = config.getSessionApiHeaders(); - assertEquals(config.apiKey, headers.get("Bugsnag-Api-Key")); + assertEquals(config.getApiKey(), headers.get("Bugsnag-Api-Key")); assertNotNull(headers.get("Bugsnag-Sent-At")); assertNotNull(headers.get("Bugsnag-Payload-Version")); } @@ -69,8 +69,8 @@ public void testEndpoints() { String sessions = "https://sessions.myexample.com"; config.setEndpoints(new EndpointConfiguration(notify, sessions)); - assertEquals(notify, getDeliveryEndpoint(config.delivery)); - assertEquals(sessions, getDeliveryEndpoint(config.sessionDelivery)); + assertEquals(notify, getDeliveryEndpoint(config.getDelivery())); + assertEquals(sessions, getDeliveryEndpoint(config.getSessionDelivery())); } @Test(expected = IllegalArgumentException.class) @@ -89,14 +89,14 @@ public void testInvalidSessionEndpoint() { EndpointConfiguration emptySession = new EndpointConfiguration("http://example.com", ""); config.setEndpoints(emptySession); assertFalse(config.shouldAutoCaptureSessions()); - assertNull(getDeliveryEndpoint(config.sessionDelivery)); + assertNull(getDeliveryEndpoint(config.getSessionDelivery())); config.setAutoCaptureSessions(true); EndpointConfiguration validSessions = new EndpointConfiguration( "http://example.com", "http://sessions.example.com"); config.setEndpoints(validSessions); assertTrue(config.shouldAutoCaptureSessions()); - assertEquals("http://sessions.example.com", getDeliveryEndpoint(config.sessionDelivery)); + assertEquals("http://sessions.example.com", getDeliveryEndpoint(config.getSessionDelivery())); } @Test @@ -121,7 +121,7 @@ public void testCustomSerializer() throws SerializationException { // flag to check if writeToStream was called final boolean[] methodCalled = {false}; - //Anonymous class extending DefaultSerializer + // Anonymous class extending DefaultSerializer Serializer customSerializer = new DefaultSerializer() { @Override public void writeToStream(OutputStream stream, Object object) throws SerializationException { @@ -150,8 +150,8 @@ public void deliver(Serializer serializer, Object object, Map he public void close() { } }; - config.delivery = delivery; - config.sessionDelivery = delivery; + config.setDelivery(delivery); + config.setSessionDelivery(delivery); ByteArrayOutputStream baos = new ByteArrayOutputStream(); System.setErr(new PrintStream(baos)); diff --git a/bugsnag/src/test/java/com/bugsnag/MetadataTest.java b/bugsnag/src/test/java/com/bugsnag/MetadataTest.java index a28c4975..983d899f 100644 --- a/bugsnag/src/test/java/com/bugsnag/MetadataTest.java +++ b/bugsnag/src/test/java/com/bugsnag/MetadataTest.java @@ -19,7 +19,7 @@ public void testEmptyMetadata() { @SuppressWarnings("unchecked") public void testSingleTabSingleValue() { Metadata metadata = new Metadata(); - metadata.addToTab("tab-name", "key-1", "value-1"); + metadata.addMetadata("tab-name", "key-1", "value-1"); assertEquals(1, metadata.size()); assertEquals(1, ((Map) metadata.get("tab-name")).size()); @@ -30,8 +30,8 @@ public void testSingleTabSingleValue() { @SuppressWarnings("unchecked") public void testSingleTabMultipleValues() { Metadata metadata = new Metadata(); - metadata.addToTab("tab-name", "key-1", "value-1"); - metadata.addToTab("tab-name", "key-2", "value-2"); + metadata.addMetadata("tab-name", "key-1", "value-1"); + metadata.addMetadata("tab-name", "key-2", "value-2"); assertEquals(1, metadata.size()); assertEquals(2, ((Map) metadata.get("tab-name")).size()); @@ -43,8 +43,8 @@ public void testSingleTabMultipleValues() { @SuppressWarnings("unchecked") public void testMultipleTabs() { Metadata metadata = new Metadata(); - metadata.addToTab("tab-name-1", "key-1", "value-1"); - metadata.addToTab("tab-name-2", "key-1", "value-1"); + metadata.addMetadata("tab-name-1", "key-1", "value-1"); + metadata.addMetadata("tab-name-2", "key-1", "value-1"); assertEquals(2, metadata.size()); assertEquals(1, ((Map) metadata.get("tab-name-1")).size()); @@ -57,12 +57,12 @@ public void testMultipleTabs() { @SuppressWarnings("unchecked") public void testClearTab() { Metadata metadata = new Metadata(); - metadata.addToTab("tab-name-1", "key-1", "value-1"); - metadata.addToTab("tab-name-2", "key-1", "value-1"); + metadata.addMetadata("tab-name-1", "key-1", "value-1"); + metadata.addMetadata("tab-name-2", "key-1", "value-1"); assertEquals(2, metadata.size()); - metadata.clearTab("tab-name-1"); + metadata.clearMetadata("tab-name-1"); assertEquals(1, metadata.size()); diff --git a/bugsnag/src/test/java/com/bugsnag/NotificationTest.java b/bugsnag/src/test/java/com/bugsnag/NotificationTest.java index 0e5c7ef1..6ebb9459 100644 --- a/bugsnag/src/test/java/com/bugsnag/NotificationTest.java +++ b/bugsnag/src/test/java/com/bugsnag/NotificationTest.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.Date; - public class NotificationTest { private Report report; @@ -29,8 +28,8 @@ public class NotificationTest { @Before public void setUp() { config = new Configuration("api-key"); - config.appVersion = "1.2.3"; - config.releaseStage = "dev"; + config.setAppVersion("1.2.3"); + config.setReleaseStage("dev"); report = new Report(config, new RuntimeException()); // Only include properties with non-null values @@ -38,8 +37,8 @@ public void setUp() { } private JsonNode generateJson(ObjectMapper mapper, - Configuration config, - Report report) throws IOException { + Configuration config, + Report report) throws IOException { Notification notification = new Notification(config, report); String json = mapper.writeValueAsString(notification); return mapper.readTree(json); diff --git a/bugsnag/src/test/java/com/bugsnag/SessionPayloadTest.java b/bugsnag/src/test/java/com/bugsnag/SessionPayloadTest.java index c60c16c1..8f7e313f 100644 --- a/bugsnag/src/test/java/com/bugsnag/SessionPayloadTest.java +++ b/bugsnag/src/test/java/com/bugsnag/SessionPayloadTest.java @@ -12,13 +12,13 @@ import java.util.Collection; import java.util.Date; - public class SessionPayloadTest { private JsonNode rootNode; /** * Initialises the session payload for serialisation + * * @throws Throwable the throwable */ @Before @@ -29,8 +29,8 @@ public void setUp() throws Throwable { sessionCount.incrementSessionsStarted(); sessionCounts.add(sessionCount); Configuration configuration = new Configuration("api-key"); - configuration.appVersion = "1.2.3"; - configuration.releaseStage = "dev"; + configuration.setAppVersion("1.2.3"); + configuration.setReleaseStage("dev"); SessionPayload payload = new SessionPayload(sessionCounts, configuration); ObjectMapper mapper = new ObjectMapper(); diff --git a/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java b/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java index 872b0956..3c67b28e 100644 --- a/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java +++ b/bugsnag/src/test/java/com/bugsnag/SessionTrackerTest.java @@ -36,7 +36,7 @@ public class SessionTrackerTest { public void setUp() { configuration = new Configuration("api-key"); sessionDelivery = new ConfigurationTest.FakeHttpDelivery(); - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); sessionTracker = new SessionTracker(configuration); assertNull(sessionTracker.getSession()); } @@ -133,16 +133,16 @@ public void run() { @Test public void disabledReleaseStage() { - configuration.enabledReleaseStages = Collections.singleton("prod"); - configuration.releaseStage = "dev"; + configuration.setEnabledReleaseStages(Collections.singleton("prod")); + configuration.setReleaseStage("dev"); sessionTracker.startSession(new Date(), false); assertNull(sessionTracker.getSession()); } @Test public void enabledReleaseStage() { - configuration.enabledReleaseStages = Collections.singleton("prod"); - configuration.releaseStage = "prod"; + configuration.setEnabledReleaseStages(Collections.singleton("prod")); + configuration.setReleaseStage("prod"); sessionTracker.startSession(new Date(), false); assertNotNull(sessionTracker.getSession()); } @@ -156,7 +156,7 @@ public void deliver(Serializer serializer, Object object, Map he fail("Should not be called if no sessions enqueued"); } }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); sessionTracker.flushSessions(new Date()); assertFalse(sessionDelivery.delivered); } @@ -170,7 +170,7 @@ public void deliver(Serializer serializer, Object object, Map he fail("Should not be called if date has not exceeded batch period"); } }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); sessionTracker.startSession(new Date(1309209859), false); sessionTracker.flushSessions(new Date(1309209859)); assertFalse(sessionDelivery.delivered); @@ -200,7 +200,7 @@ public void deliver(Serializer serializer, Object object, Map he assertEquals("1970-01-18T11:13:00Z", sessionCount.getStartedAt()); } }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); sessionTracker.startSession(new Date(5092340L), false); sessionTracker.startSession(new Date(125098234L), false); sessionTracker.startSession(new Date(1509207501L), false); @@ -224,7 +224,7 @@ public void deliver(Serializer serializer, Object object, Map he assertEquals("1970-01-01T02:46:00Z", sessionCount.getStartedAt()); } }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); // 2 mins apart sessionTracker.startSession(new Date(10000000L), false); @@ -247,7 +247,7 @@ public void deliver(Serializer serializer, Object object, Map he assertEquals("1970-01-01T02:46:00Z", sessionCount.getStartedAt()); } }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); // 1 hour apart sessionTracker.startSession(new Date(10000000L), false); @@ -270,7 +270,7 @@ public void deliver(Serializer serializer, Object object, Map he assertEquals("1970-01-01T02:46:00Z", sessionCount.getStartedAt()); } }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); // 1 hour apart sessionTracker.startSession(new Date(10000000L), false); @@ -284,7 +284,7 @@ public void deliver(Serializer serializer, Object object, Map he public void zeroSessionCount() { CustomDelivery sessionDelivery = new CustomDelivery() { }; - configuration.sessionDelivery = sessionDelivery; + configuration.setSessionDelivery(sessionDelivery); sessionTracker.flushSessions(new Date(10120000L)); sessionTracker.flushSessions(new Date(14000000L)); assertFalse(sessionDelivery.delivered); @@ -301,7 +301,7 @@ public void testSessionShutdownStartSession() { public void testSessionShutdownDelivers() { CustomDelivery delivery = new CustomDelivery() { }; - configuration.sessionDelivery = delivery; + configuration.setSessionDelivery(delivery); sessionTracker.startSession(new Date(), true); sessionTracker.shutdown(); @@ -313,7 +313,7 @@ public void testSessionShutdownDelivers() { public void testMultiShutdown() { CustomDelivery delivery = new CustomDelivery() { }; - configuration.sessionDelivery = delivery; + configuration.setSessionDelivery(delivery); sessionTracker.startSession(new Date(), true); sessionTracker.shutdown(); @@ -332,7 +332,7 @@ public void deliver(Serializer serializer, Object object, Map he fail("Delivery should be suppressed by OnSession callback returning false"); } }; - configuration.sessionDelivery = delivery; + configuration.setSessionDelivery(delivery); // Add callback which returns false to suppress sending sessionTracker.addOnSession(new OnSession() { From e343ab9fb520d2e2116b9f4cb18f32fa411d1bac Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Tue, 2 Dec 2025 10:48:17 +0100 Subject: [PATCH 10/10] missing metadata accessors (#246) --- .../src/main/java/com/bugsnag/BugsnagAppender.java | 8 ++++---- bugsnag/src/main/java/com/bugsnag/Report.java | 6 +++--- .../java/com/bugsnag/callbacks/DeviceCallback.java | 4 ++-- .../bugsnag/callbacks/JakartaServletCallback.java | 10 +++++----- .../test/java/com/bugsnag/AppenderMetadataTest.java | 2 +- bugsnag/src/test/java/com/bugsnag/BugsnagTest.java | 10 +++++----- .../com/bugsnag/example/logback/cli/Application.java | 12 ++++++------ .../java/com/bugsnag/example/simple/ExampleApp.java | 10 +++++----- .../spring/web/ApplicationRestController.java | 2 +- .../java/com/bugsnag/example/spring/web/Config.java | 8 ++++---- .../spring/cli/ApplicationCommandLineRunner.java | 2 +- .../java/com/bugsnag/example/spring/cli/Config.java | 8 ++++---- .../scenarios/ScheduledTaskExecutorScenario.java | 4 ++-- .../mazerunner/scenarios/AutoRedactScenario.java | 6 +++--- .../scenarios/LogbackMetadataScenario.java | 6 +++--- .../mazerunner/scenarios/ManualRedactScenario.java | 6 +++--- .../mazerunner/scenarios/MetadataScenario.java | 2 +- .../mazerunner/scenarios/ThreadMetadataScenario.java | 6 +++--- .../scenarios/UnhandledThreadMetadataScenario.java | 4 ++-- 19 files changed, 58 insertions(+), 58 deletions(-) diff --git a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java index 7fe93bfd..07f42b56 100644 --- a/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java +++ b/bugsnag/src/main/java/com/bugsnag/BugsnagAppender.java @@ -137,9 +137,9 @@ protected void append(final ILoggingEvent event) { public boolean onError(Report report) { // Add some data from the logging event - report.addToTab("Log event data", + report.addMetadata("Log event data", "Message", event.getFormattedMessage()); - report.addToTab("Log event data", + report.addMetadata("Log event data", "Logger name", event.getLoggerName()); // Add details from the logging context to the event @@ -171,7 +171,7 @@ private void populateContextData(Report report, ILoggingEvent event) { // Loop through all the keys and put them in the correct tabs for (Map.Entry entry : propertyMap.entrySet()) { - report.addToTab("Context", entry.getKey(), entry.getValue()); + report.addMetadata("Context", entry.getKey(), entry.getValue()); } } } @@ -279,7 +279,7 @@ public boolean onError(Report report) { for (LogbackMetadata metadata : globalMetadata) { for (LogbackMetadataTab tab : metadata.getTabs()) { for (LogbackMetadataKey key : tab.getKeys()) { - report.addToTab(tab.getName(), + report.addMetadata(tab.getName(), key.getName(), key.getValue()); } diff --git a/bugsnag/src/main/java/com/bugsnag/Report.java b/bugsnag/src/main/java/com/bugsnag/Report.java index 53f23630..01899032 100644 --- a/bugsnag/src/main/java/com/bugsnag/Report.java +++ b/bugsnag/src/main/java/com/bugsnag/Report.java @@ -189,7 +189,7 @@ public String getExceptionMessage() { * @param value the metadata value to add * @return the modified report */ - public Report addToTab(String tabName, String key, Object value) { + public Report addMetadata(String tabName, String key, Object value) { diagnostics.metadata.addMetadata(tabName, key, value); return this; } @@ -211,7 +211,7 @@ public Report clearTab(String tabName) { * @param key the key of app info to add * @param value the value of app info to add * @return the modified report - * @deprecated use {@link #addToTab(String, String, Object)} instead + * @deprecated use {@link #addMetadata(String, String, Object)} instead */ @Deprecated public Report setAppInfo(String key, Object value) { @@ -256,7 +256,7 @@ public Report setContext(String context) { * @param key the key of device info to add * @param value the value of device info to add * @return the modified report - * @deprecated use {@link #addToTab(String, String, Object)} instead + * @deprecated use {@link #addMetadata(String, String, Object)} instead */ @Deprecated public Report setDeviceInfo(String key, Object value) { diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java index 59005e59..e13a3b48 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/DeviceCallback.java @@ -86,8 +86,8 @@ public void run() { @Override public boolean onError(Report report) { report - .addToTab("device", "osArch", System.getProperty("os.arch")) - .addToTab("device", "locale", Locale.getDefault()) + .addMetadata("device", "osArch", System.getProperty("os.arch")) + .addMetadata("device", "locale", Locale.getDefault()) .setDeviceInfo("hostname", getHostnameValue()) .setDeviceInfo("osName", System.getProperty("os.name")) .setDeviceInfo("osVersion", System.getProperty("os.version")); diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java index b11dd73b..c05ce075 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java @@ -35,12 +35,12 @@ public boolean onError(Report report) { // Add request information to metadata report - .addToTab("request", "url", request.getRequestURL().toString()) - .addToTab("request", "method", request.getMethod()) - .addToTab("request", "params", + .addMetadata("request", "url", request.getRequestURL().toString()) + .addMetadata("request", "method", request.getMethod()) + .addMetadata("request", "params", new HashMap(request.getParameterMap())) - .addToTab("request", "clientIp", getClientIp(request)) - .addToTab("request", "headers", getHeaderMap(request)); + .addMetadata("request", "clientIp", getClientIp(request)) + .addMetadata("request", "headers", getHeaderMap(request)); // Set default context if (report.getContext() == null) { diff --git a/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java b/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java index 682087c5..f192b986 100644 --- a/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java +++ b/bugsnag/src/test/java/com/bugsnag/AppenderMetadataTest.java @@ -109,7 +109,7 @@ public void testMetadataRemoval() { // Send three test logs, the first one with report metadata added LOGGER.warn(new BugsnagMarker(report -> { - report.addToTab("report", "some key", "some report value"); + report.addMetadata("report", "some key", "some report value"); return true; }), "Test exception", new RuntimeException("test")); diff --git a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java index 10d19d49..23d7db97 100644 --- a/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java +++ b/bugsnag/src/test/java/com/bugsnag/BugsnagTest.java @@ -186,10 +186,10 @@ public void close() { } }); assertTrue(bugsnag.notify(new Throwable(), report -> { - report.addToTab("firsttab", "testredact1", "secretpassword"); - report.addToTab("firsttab", "testredact2", "secretpassword"); - report.addToTab("firsttab", "testredact3", "secretpassword"); - report.addToTab("secondtab", "testredact1", "secretpassword"); + report.addMetadata("firsttab", "testredact1", "secretpassword"); + report.addMetadata("firsttab", "testredact2", "secretpassword"); + report.addMetadata("firsttab", "testredact3", "secretpassword"); + report.addMetadata("secondtab", "testredact1", "secretpassword"); return true; })); } @@ -225,7 +225,7 @@ public void close() { headers.put("Cookie", "123456ABCDEF"); headers.put("cookie", "123456ABCDEF"); - report.addToTab("request", "headers", headers); + report.addMetadata("request", "headers", headers); return true; })); } diff --git a/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java b/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java index a15208c1..f7a817f9 100644 --- a/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java +++ b/examples/logback/src/main/java/com/bugsnag/example/logback/cli/Application.java @@ -22,10 +22,10 @@ public static void main(String[] args) throws Exception { if (appender instanceof BugsnagAppender) { // Set some global meta data (added to each report) ((BugsnagAppender) appender).getClient().addCallback((report) -> { - report.addToTab("diagnostics", "timestamp", new Date()); - report.addToTab("customer", "name", "acme-inc"); - report.addToTab("customer", "paying", true); - report.addToTab("customer", "spent", 1234); + report.addMetadata("diagnostics", "timestamp", new Date()); + report.addMetadata("customer", "name", "acme-inc"); + report.addMetadata("customer", "paying", true); + report.addMetadata("customer", "spent", 1234); report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); @@ -58,8 +58,8 @@ public static void main(String[] args) throws Exception { throw new RuntimeException("Handled exception - custom metadata"); } catch (RuntimeException e) { LOGGER.warn(new BugsnagMarker((report) -> { - report.addToTab("report tab", "data key 1", "data value 1"); - report.addToTab("report tab", "data key 2", "data value 2"); + report.addMetadata("report tab", "data key 1", "data value 1"); + report.addMetadata("report tab", "data key 2", "data value 2"); return true; }), "Something bad happened", e); } diff --git a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java index 41059c11..47047e97 100644 --- a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java +++ b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java @@ -23,10 +23,10 @@ public static void main(String[] args) throws InterruptedException { bugsnag.addCallback(new Callback() { @Override public boolean onError(Report report) { - report.addToTab("diagnostics", "timestamp", new Date()); - report.addToTab("customer", "name", "acme-inc"); - report.addToTab("customer", "paying", true); - report.addToTab("customer", "spent", 1234); + report.addMetadata("diagnostics", "timestamp", new Date()); + report.addMetadata("customer", "name", "acme-inc"); + report.addMetadata("customer", "paying", true); + report.addMetadata("customer", "spent", 1234); report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); @@ -56,7 +56,7 @@ public boolean onError(Report report) { @Override public boolean onError(Report report) { report.setSeverity(Severity.WARNING); - report.addToTab("report", "something", "that happened"); + report.addMetadata("report", "something", "that happened"); report.setContext("the context"); return true; } diff --git a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java index 584934d8..e6dffd14 100644 --- a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java +++ b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/ApplicationRestController.java @@ -73,7 +73,7 @@ public String sendHandledExceptionWithMetadata() { @Override public boolean onError(Report report) { report.setSeverity(Severity.WARNING); - report.addToTab("report", "something", "that happened"); + report.addMetadata("report", "something", "that happened"); report.setContext("the context"); return true; } diff --git a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java index e7385514..779bb98d 100644 --- a/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java +++ b/examples/spring-web/src/main/java/com/bugsnag/example/spring/web/Config.java @@ -32,10 +32,10 @@ public Bugsnag bugsnag() { bugsnag.addCallback(new Callback() { @Override public boolean onError(Report report) { - report.addToTab("diagnostics", "timestamp", new Date()); - report.addToTab("customer", "name", "acme-inc"); - report.addToTab("customer", "paying", true); - report.addToTab("customer", "spent", 1234); + report.addMetadata("diagnostics", "timestamp", new Date()); + report.addMetadata("customer", "name", "acme-inc"); + report.addMetadata("customer", "paying", true); + report.addMetadata("customer", "spent", 1234); report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); diff --git a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java index 2f26233d..307471c1 100644 --- a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java +++ b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/ApplicationCommandLineRunner.java @@ -52,7 +52,7 @@ public void run(final String... args) throws Exception { @Override public boolean onError(Report report) { report.setSeverity(Severity.WARNING); - report.addToTab("report", "something", "that happened"); + report.addMetadata("report", "something", "that happened"); report.setContext("the context"); return true; } diff --git a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java index 3aff0375..c233813b 100644 --- a/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java +++ b/examples/spring/src/main/java/com/bugsnag/example/spring/cli/Config.java @@ -33,10 +33,10 @@ public Bugsnag bugsnag() { bugsnag.addCallback(new Callback() { @Override public boolean onError(Report report) { - report.addToTab("diagnostics", "timestamp", new Date()); - report.addToTab("customer", "name", "acme-inc"); - report.addToTab("customer", "paying", true); - report.addToTab("customer", "spent", 1234); + report.addMetadata("diagnostics", "timestamp", new Date()); + report.addMetadata("customer", "name", "acme-inc"); + report.addMetadata("customer", "paying", true); + report.addMetadata("customer", "spent", 1234); report.setUserName("User Name"); report.setUserEmail("user@example.com"); report.setUserId("12345"); diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java index 3ab87cd8..62ca334b 100644 --- a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java @@ -28,8 +28,8 @@ public void run() { bugsnag.notify(new RuntimeException("Whoops"), new Callback() { @Override public boolean onError(Report report) { - report.addToTab("executor", "multiThreaded", threadnames.size() > 1); - report.addToTab("executor", "names", threadnames); + report.addMetadata("executor", "multiThreaded", threadnames.size() > 1); + report.addMetadata("executor", "names", threadnames); return true; } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java index 3615278c..8091c2ae 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/AutoRedactScenario.java @@ -18,9 +18,9 @@ public void run() { bugsnag.notify(generateException(), new Callback() { @Override public boolean onError(Report report) { - report.addToTab("user", "password", "hunter2"); - report.addToTab("custom", "password", "hunter2"); - report.addToTab("custom", "foo", "hunter2"); + report.addMetadata("user", "password", "hunter2"); + report.addMetadata("custom", "password", "hunter2"); + report.addMetadata("custom", "foo", "hunter2"); return true; } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java index 4f19a002..f9d2c257 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/LogbackMetadataScenario.java @@ -24,9 +24,9 @@ public void run() { LOGGER.warn(new BugsnagMarker(new Callback() { @Override public boolean onError(Report report) { - report.addToTab("user", "foo", "hunter2"); - report.addToTab("custom", "foo", "hunter2"); - report.addToTab("custom", "bar", "hunter2"); + report.addMetadata("user", "foo", "hunter2"); + report.addMetadata("custom", "foo", "hunter2"); + report.addMetadata("custom", "bar", "hunter2"); return true; } }), "Error sent to Bugsnag using the logback appender", generateException()); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java index b518d3dd..b6bc1636 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ManualRedactScenario.java @@ -21,9 +21,9 @@ public void run() { bugsnag.notify(generateException(), new Callback() { @Override public boolean onError(Report report) { - report.addToTab("user", "foo", "hunter2"); - report.addToTab("custom", "foo", "hunter2"); - report.addToTab("custom", "bar", "hunter2"); + report.addMetadata("user", "foo", "hunter2"); + report.addMetadata("custom", "foo", "hunter2"); + report.addMetadata("custom", "bar", "hunter2"); return true; } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java index afe44e3e..436b8120 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/MetadataScenario.java @@ -18,7 +18,7 @@ public void run() { bugsnag.notify(generateException(), new Callback() { @Override public boolean onError(Report report) { - report.addToTab("Custom", "foo", "Hello World!"); + report.addMetadata("Custom", "foo", "Hello World!"); return true; } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java index 81252d8c..2e8aafa1 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/ThreadMetadataScenario.java @@ -19,8 +19,8 @@ public void run() { bugsnag.addCallback(new Callback() { @Override public boolean onError(Report report) { - report.addToTab("Custom", "test", "Global value"); - report.addToTab("Custom", "foo", "Global value to be overwritten"); + report.addMetadata("Custom", "test", "Global value"); + report.addMetadata("Custom", "foo", "Global value to be overwritten"); return true; } }); @@ -49,7 +49,7 @@ public void run() { bugsnag.notify(generateException(), new Callback() { @Override public boolean onError(Report report) { - report.addToTab("Custom", "bar", "Hello World!"); + report.addMetadata("Custom", "bar", "Hello World!"); return true; } }); diff --git a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java index f4b9afe4..0121d62e 100644 --- a/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java +++ b/features/fixtures/scenarios/src/main/java/com/bugsnag/mazerunner/scenarios/UnhandledThreadMetadataScenario.java @@ -19,8 +19,8 @@ public void run() { bugsnag.addCallback(new Callback() { @Override public boolean onError(Report report) { - report.addToTab("Custom", "test", "Global value"); - report.addToTab("Custom", "foo", "Global value to be overwritten"); + report.addMetadata("Custom", "test", "Global value"); + report.addMetadata("Custom", "foo", "Global value to be overwritten"); return true; } });