From 57625387ef11b8a6e0a18a4d0df56d3e5196ac4c Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 25 May 2026 17:30:59 -0300 Subject: [PATCH 01/10] feat(java-agent): add rollbar-java-agent module skeleton --- rollbar-java-agent/build.gradle.kts | 55 +++++++++++++++++ .../rollbar/agent/AgentTelemetryStore.java | 27 +++++++++ .../com/rollbar/agent/NetworkEventBridge.java | 60 +++++++++++++++++++ .../java/com/rollbar/agent/RollbarAgent.java | 54 +++++++++++++++++ .../java/com/rollbar/agent/UrlSanitizer.java | 33 ++++++++++ settings.gradle.kts | 6 ++ 6 files changed, 235 insertions(+) create mode 100644 rollbar-java-agent/build.gradle.kts create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/UrlSanitizer.java diff --git a/rollbar-java-agent/build.gradle.kts b/rollbar-java-agent/build.gradle.kts new file mode 100644 index 00000000..34facdef --- /dev/null +++ b/rollbar-java-agent/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + `java-library` + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +dependencies { + implementation("net.bytebuddy:byte-buddy:1.14.18") + implementation("net.bytebuddy:byte-buddy-agent:1.14.18") + api(project(":rollbar-api")) + implementation(project(":rollbar-java")) + compileOnly("org.apache.httpcomponents:httpclient:4.5.14") + compileOnly("org.apache.httpcomponents.client5:httpclient5:5.3.1") + + testImplementation(platform("org.junit:junit-bom:5.14.3")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.mockito:mockito-core:5.11.0") + testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") + testImplementation("org.apache.httpcomponents:httpclient:4.5.14") + testImplementation("org.apache.httpcomponents.client5:httpclient5:5.3.1") +} + +tasks.jar { + manifest { + attributes( + "Premain-Class" to "com.rollbar.agent.RollbarAgent", + "Agent-Class" to "com.rollbar.agent.RollbarAgent", + "Can-Redefine-Classes" to "true", + "Can-Retransform-Classes" to "true" + ) + } +} + +tasks.shadowJar { + archiveClassifier.set("") + relocate("net.bytebuddy", "com.rollbar.agent.shaded.bytebuddy") + mergeServiceFiles() +} + +// Override root's Java 8 compatibility — this agent targets Java 11+ to support +// java.net.http.HttpClient instrumentation. +tasks.withType().configureEach { + options.release.set(11) +} + +tasks.test { + useJUnitPlatform() + val agentJar = tasks.shadowJar.get().archiveFile.get().asFile + // Load as Java agent (instruments HTTP classes on startup) + jvmArgs("-javaagent:$agentJar") + // Also put on test classpath — the TCCL reflection bridge finds agent classes via the + // system classloader; mirrors production use where rollbar-java-agent is a Gradle/Maven dep + classpath += files(agentJar) + dependsOn(tasks.shadowJar) +} diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java new file mode 100644 index 00000000..35bf036f --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java @@ -0,0 +1,27 @@ +package com.rollbar.agent; + +import com.rollbar.api.payload.data.TelemetryEvent; +import com.rollbar.notifier.telemetry.RollbarTelemetryEventTracker; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; + +import java.util.List; + +public final class AgentTelemetryStore { + + private static volatile TelemetryEventTracker INSTANCE = + new RollbarTelemetryEventTracker(System::currentTimeMillis, 100); + + private AgentTelemetryStore() {} + + public static TelemetryEventTracker getInstance() { + return INSTANCE; + } + + public static List getAll() { + return INSTANCE.getAll(); + } + + public static void resetForTesting() { + INSTANCE = new RollbarTelemetryEventTracker(System::currentTimeMillis, 100); + } +} diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java new file mode 100644 index 00000000..947c5b34 --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java @@ -0,0 +1,60 @@ +package com.rollbar.agent; + +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Called by JDK-class advice via reflection to bridge the classloader gap. + * + *

ByteBuddy advice inlined into bootstrap/platform classloader classes (e.g. + * {@code HttpURLConnection}, {@code HttpClient}) cannot directly reference application-classloader + * classes. Advice code uses {@code Thread.currentThread().getContextClassLoader().loadClass(...)} + * to reach this class and delegates all Rollbar-specific logic here. + */ +public final class NetworkEventBridge { + + // Tracks connections/responses already recorded to deduplicate re-entrant calls. + // WeakHashMap so entries are garbage-collected when the connection is released. + private static final Set RECORDED = Collections.newSetFromMap( + Collections.synchronizedMap(new WeakHashMap<>()) + ); + + private NetworkEventBridge() {} + + public static void resetRecordedForTesting() { + RECORDED.clear(); + } + + /** + * Marks the given key as recorded. Returns {@code true} if this is the first time, + * {@code false} if already recorded (duplicate/re-entrant call). + */ + public static boolean markAsRecorded(Object key) { + return RECORDED.add(key); + } + + public static void recordNetworkEvent(Object key, String method, String url, String statusCode) { + if (!markAsRecorded(key)) { + return; // deduplicate re-entrant calls for the same connection + } + AgentTelemetryStore.getInstance().recordNetworkEventFor( + Level.CRITICAL, + Source.SERVER, + method, + UrlSanitizer.sanitize(url), + statusCode + ); + } + + public static void recordError(String message) { + AgentTelemetryStore.getInstance().recordManualEventFor( + Level.CRITICAL, + Source.SERVER, + "Network error: " + (message != null ? message : "unknown") + ); + } +} diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java new file mode 100644 index 00000000..8eb04d68 --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java @@ -0,0 +1,54 @@ +package com.rollbar.agent; + +import com.rollbar.agent.instrumentation.ApacheHttpClient4Instrumentation; +import com.rollbar.agent.instrumentation.ApacheHttpClient5Instrumentation; +import com.rollbar.agent.instrumentation.HttpURLConnectionInstrumentation; +import com.rollbar.agent.instrumentation.JavaHttpClientInstrumentation; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.instrument.Instrumentation; + +/** + * Java agent entry point. Attach with {@code -javaagent:/path/to/rollbar-java-agent.jar}. + * + *

Wire into your Rollbar configuration with: + *

+ *   Rollbar.init(withAccessToken("...")
+ *       .telemetryEventTracker(RollbarAgent.getTelemetryTracker())
+ *       .build());
+ * 
+ */ +public class RollbarAgent { + + private RollbarAgent() {} + + public static void premain(String args, Instrumentation inst) { + installInstrumentation(inst); + } + + public static void agentmain(String args, Instrumentation inst) { + installInstrumentation(inst); + } + + private static void installInstrumentation(Instrumentation inst) { + // Override ByteBuddy's default which ignores all java.* and javax.* classes, + // so we can instrument JDK HTTP clients (HttpURLConnection, HttpClient). + // We still ignore ByteBuddy's own classes to avoid instrumentation loops. + AgentBuilder builder = new AgentBuilder.Default() + .ignore(ElementMatchers.nameStartsWith("net.bytebuddy.") + .or(ElementMatchers.nameStartsWith("com.rollbar.agent.shaded."))) + .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) + .with(AgentBuilder.TypeStrategy.Default.REDEFINE); + + HttpURLConnectionInstrumentation.install(builder, inst); + JavaHttpClientInstrumentation.installIfAvailable(builder, inst); + ApacheHttpClient4Instrumentation.installIfAvailable(builder, inst); + ApacheHttpClient5Instrumentation.installIfAvailable(builder, inst); + } + + public static TelemetryEventTracker getTelemetryTracker() { + return AgentTelemetryStore.getInstance(); + } +} diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/UrlSanitizer.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/UrlSanitizer.java new file mode 100644 index 00000000..d11ae48d --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/UrlSanitizer.java @@ -0,0 +1,33 @@ +package com.rollbar.agent; + +import java.net.URI; +import java.net.URISyntaxException; + +public final class UrlSanitizer { + + private UrlSanitizer() {} + + /** + * Strips userinfo, query parameters, and fragment from the URL, leaving only + * scheme, host, port, and path. + */ + public static String sanitize(String rawUrl) { + if (rawUrl == null) { + return null; + } + try { + URI uri = new URI(rawUrl); + return new URI( + uri.getScheme(), + null, + uri.getHost(), + uri.getPort(), + uri.getPath(), + null, + null + ).toString(); + } catch (URISyntaxException e) { + return rawUrl; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 73e10c5b..93e158bf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,3 +67,9 @@ if (isJava8 || isJava11) { println("Java ${JavaVersion.current()} detected: including Android modules") include(":rollbar-android", ":examples:rollbar-android") } + +if (isJava8) { + println("Java 8 detected: excluding :rollbar-java-agent (requires Java 11+)") +} else { + include(":rollbar-java-agent") +} From 70573e5090fdfc389765c79ee6bc9d72e77e0414 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 25 May 2026 17:31:14 -0300 Subject: [PATCH 02/10] feat(java-agent): instrument HttpURLConnection and java.net.http.HttpClient --- .../HttpURLConnectionInstrumentation.java | 69 ++++++++++++++++ .../JavaHttpClientInstrumentation.java | 80 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java new file mode 100644 index 00000000..b2a8c513 --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java @@ -0,0 +1,69 @@ +package com.rollbar.agent.instrumentation; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.instrument.Instrumentation; + +public final class HttpURLConnectionInstrumentation { + + private HttpURLConnectionInstrumentation() {} + + public static void install(AgentBuilder builder, Instrumentation inst) { + builder + .type(ElementMatchers.named("java.net.HttpURLConnection")) + .transform((b, typeDescription, classLoader, module, protectionDomain) -> + b.visit(Advice.to(GetResponseCodeAdvice.class) + .on(ElementMatchers.named("getResponseCode"))) + ) + .installOn(inst); + } + + /** + * Advice inlined into {@code java.net.HttpURLConnection.getResponseCode()}. + * + *

Only JDK types are referenced directly. The Rollbar bridge is reached via TCCL + * to cross the classloader boundary. The connection instance is used as the deduplication + * key — getResponseCode() is called re-entrantly up to 3 times per request internally. + */ + public static class GetResponseCodeAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onExit( + @Advice.This Object connection, + @Advice.Return int statusCode, + @Advice.Thrown Throwable thrown + ) { + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = ClassLoader.getSystemClassLoader(); + } + Class bridge = cl.loadClass("com.rollbar.agent.NetworkEventBridge"); + + if (thrown != null) { + Boolean recorded = (Boolean) bridge + .getMethod("markAsRecorded", Object.class).invoke(null, thrown); + if (recorded) { + String msg = thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName(); + bridge.getMethod("recordError", String.class).invoke(null, msg); + } + return; + } + + if (statusCode >= 400) { + Object url = connection.getClass().getMethod("getURL").invoke(connection); + String urlStr = url != null ? url.toString() : ""; + String method = (String) connection.getClass().getMethod("getRequestMethod").invoke(connection); + // connection instance as dedup key — same object across all re-entrant getResponseCode() calls + bridge.getMethod("recordNetworkEvent", + Object.class, String.class, String.class, String.class) + .invoke(null, connection, method, urlStr, String.valueOf(statusCode)); + } + } catch (Throwable ignored) { + // Advice must never throw — swallow all errors including Error subclasses + } + } + } +} diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java new file mode 100644 index 00000000..5247f832 --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java @@ -0,0 +1,80 @@ +package com.rollbar.agent.instrumentation; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.instrument.Instrumentation; +import java.net.http.HttpClient; + +public final class JavaHttpClientInstrumentation { + + private JavaHttpClientInstrumentation() {} + + public static void installIfAvailable(AgentBuilder builder, Instrumentation inst) { + try { + Class.forName("java.net.http.HttpClient"); + } catch (ClassNotFoundException e) { + return; + } + + builder + .type(ElementMatchers.isSubTypeOf(HttpClient.class)) + .transform((b, typeDescription, classLoader, module, protectionDomain) -> + b.visit(Advice.to(SendAdvice.class) + .on(ElementMatchers.named("send"))) + ) + .installOn(inst); + } + + /** + * Advice inlined into JDK's HttpClient concrete implementation's send(). + * + *

Only JDK types are referenced directly. The Rollbar bridge is reached via TCCL + * to cross the classloader boundary. The response object is used as a deduplication key + * since both HttpClientFacade and HttpClientImpl instrument send() and the same response + * object flows through both. + */ + public static class SendAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onExit( + @Advice.Argument(0) Object request, + @Advice.Return Object response, + @Advice.Thrown Throwable thrown + ) { + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = ClassLoader.getSystemClassLoader(); + } + Class bridge = cl.loadClass("com.rollbar.agent.NetworkEventBridge"); + + if (thrown != null) { + Boolean recorded = (Boolean) bridge + .getMethod("markAsRecorded", Object.class).invoke(null, thrown); + if (recorded) { + String msg = thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName(); + bridge.getMethod("recordError", String.class).invoke(null, msg); + } + return; + } + + if (response != null) { + int statusCode = (Integer) response.getClass().getMethod("statusCode").invoke(response); + if (statusCode >= 400) { + Object uri = request.getClass().getMethod("uri").invoke(request); + String method = (String) request.getClass().getMethod("method").invoke(request); + // response object is the dedup key — unique per send() call, shared between + // HttpClientFacade and HttpClientImpl so only one event is recorded + bridge.getMethod("recordNetworkEvent", + Object.class, String.class, String.class, String.class) + .invoke(null, response, method, uri.toString(), String.valueOf(statusCode)); + } + } + } catch (Throwable ignored) { + // Advice must never throw — swallow all errors including Error subclasses + } + } + } +} From 3ee326df2f8f3f9b4826f1097d83ba4d6a537c5c Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 25 May 2026 17:31:27 -0300 Subject: [PATCH 03/10] feat(java-agent): instrument Apache HttpClient 4.x and 5.x --- .../ApacheHttpClient4Instrumentation.java | 78 +++++++++++++++++++ .../ApacheHttpClient5Instrumentation.java | 78 +++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java create mode 100644 rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java new file mode 100644 index 00000000..be57fabf --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java @@ -0,0 +1,78 @@ +package com.rollbar.agent.instrumentation; + +import com.rollbar.agent.AgentTelemetryStore; +import com.rollbar.agent.UrlSanitizer; +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; + +import java.lang.instrument.Instrumentation; + +public final class ApacheHttpClient4Instrumentation { + + private ApacheHttpClient4Instrumentation() {} + + public static void installIfAvailable(AgentBuilder builder, Instrumentation inst) { + try { + Class.forName("org.apache.http.impl.client.CloseableHttpClient"); + } catch (ClassNotFoundException e) { + return; + } + + builder + .type(ElementMatchers.hasSuperType( + ElementMatchers.named("org.apache.http.impl.client.CloseableHttpClient"))) + .transform((b, typeDescription, classLoader, module, protectionDomain) -> + b.visit(Advice.to(ExecuteAdvice.class) + .on(ElementMatchers.named("execute") + .and(ElementMatchers.takesArgument(0, + ElementMatchers.named("org.apache.http.client.methods.HttpUriRequest"))))) + ) + .installOn(inst); + } + + /** + * Apache HC 4.x runs in the application classloader, so we can reference Rollbar classes + * directly without the TCCL reflection bridge. + */ + public static class ExecuteAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onExit( + @Advice.Argument(0) HttpUriRequest request, + @Advice.Return HttpResponse response, + @Advice.Thrown Throwable thrown + ) { + try { + if (thrown != null) { + AgentTelemetryStore.getInstance().recordManualEventFor( + Level.CRITICAL, + Source.SERVER, + "Network error: " + (thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName()) + ); + return; + } + + if (response != null) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode >= 400) { + String url = request.getURI() != null ? request.getURI().toString() : ""; + AgentTelemetryStore.getInstance().recordNetworkEventFor( + Level.CRITICAL, + Source.SERVER, + request.getMethod(), + UrlSanitizer.sanitize(url), + String.valueOf(statusCode) + ); + } + } + } catch (Throwable ignored) { + // Advice must never throw + } + } + } +} diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java new file mode 100644 index 00000000..95d6e8c4 --- /dev/null +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java @@ -0,0 +1,78 @@ +package com.rollbar.agent.instrumentation; + +import com.rollbar.agent.AgentTelemetryStore; +import com.rollbar.agent.UrlSanitizer; +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; + +import java.lang.instrument.Instrumentation; + +public final class ApacheHttpClient5Instrumentation { + + private ApacheHttpClient5Instrumentation() {} + + public static void installIfAvailable(AgentBuilder builder, Instrumentation inst) { + try { + Class.forName("org.apache.hc.client5.http.impl.classic.CloseableHttpClient"); + } catch (ClassNotFoundException e) { + return; + } + + builder + .type(ElementMatchers.hasSuperType( + ElementMatchers.named("org.apache.hc.client5.http.impl.classic.CloseableHttpClient"))) + .transform((b, typeDescription, classLoader, module, protectionDomain) -> + b.visit(Advice.to(ExecuteAdvice.class) + .on(ElementMatchers.named("execute") + .and(ElementMatchers.takesArgument(0, + ElementMatchers.named("org.apache.hc.core5.http.ClassicHttpRequest"))))) + ) + .installOn(inst); + } + + /** + * Apache HC 5.x runs in the application classloader, so we can reference Rollbar classes + * directly without the TCCL reflection bridge. + */ + public static class ExecuteAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onExit( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Return ClassicHttpResponse response, + @Advice.Thrown Throwable thrown + ) { + try { + if (thrown != null) { + AgentTelemetryStore.getInstance().recordManualEventFor( + Level.CRITICAL, + Source.SERVER, + "Network error: " + (thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName()) + ); + return; + } + + if (response != null) { + int statusCode = response.getCode(); + if (statusCode >= 400) { + String url = request.getRequestUri() != null ? request.getRequestUri() : ""; + AgentTelemetryStore.getInstance().recordNetworkEventFor( + Level.CRITICAL, + Source.SERVER, + request.getMethod(), + UrlSanitizer.sanitize(url), + String.valueOf(statusCode) + ); + } + } + } catch (Throwable ignored) { + // Advice must never throw + } + } + } +} From e5226c131e4097da590bd11875ee897f4a64f641 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 25 May 2026 17:31:40 -0300 Subject: [PATCH 04/10] test(java-agent): add integration tests for instrumented HTTP clients --- .../com/rollbar/agent/RollbarAgentTest.java | 39 ++++++ .../HttpURLConnectionInstrumentationTest.java | 113 ++++++++++++++++++ .../JavaHttpClientInstrumentationTest.java | 106 ++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java create mode 100644 rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java create mode 100644 rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java new file mode 100644 index 00000000..0efbc44c --- /dev/null +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java @@ -0,0 +1,39 @@ +package com.rollbar.agent; + +import com.rollbar.notifier.telemetry.TelemetryEventTracker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class RollbarAgentTest { + + @BeforeEach + public void setUp() { + AgentTelemetryStore.resetForTesting(); + } + + @Test + public void getTelemetryTracker_returnsSingletonInstance() { + TelemetryEventTracker first = RollbarAgent.getTelemetryTracker(); + TelemetryEventTracker second = RollbarAgent.getTelemetryTracker(); + assertSame(first, second); + } + + @Test + public void urlSanitizer_stripsQueryAndFragment() { + String sanitized = UrlSanitizer.sanitize("https://api.example.com/path?token=secret#section"); + assertEquals("https://api.example.com/path", sanitized); + } + + @Test + public void urlSanitizer_handlesNullGracefully() { + assertNull(UrlSanitizer.sanitize(null)); + } + + @Test + public void urlSanitizer_handlesInvalidUrl() { + String raw = "not a url"; + assertEquals(raw, UrlSanitizer.sanitize(raw)); + } +} diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java new file mode 100644 index 00000000..6044e832 --- /dev/null +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java @@ -0,0 +1,113 @@ +package com.rollbar.agent.instrumentation; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.rollbar.agent.AgentTelemetryStore; +import com.rollbar.agent.NetworkEventBridge; +import com.rollbar.api.payload.data.TelemetryEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +public class HttpURLConnectionInstrumentationTest { + + private WireMockServer server; + + @BeforeEach + public void setUp() { + server = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + server.start(); + AgentTelemetryStore.resetForTesting(); + NetworkEventBridge.resetRecordedForTesting(); + } + + @AfterEach + public void tearDown() { + server.stop(); + } + + @Test + public void successResponse_doesNotRecordEvent() throws IOException { + server.stubFor(get(urlEqualTo("/ok")).willReturn(aResponse().withStatus(200))); + + makeRequest("GET", "/ok"); + + assertTrue(AgentTelemetryStore.getInstance().getAll().isEmpty()); + } + + @Test + public void clientErrorResponse_recordsNetworkEvent() throws IOException { + server.stubFor(get(urlEqualTo("/not-found")).willReturn(aResponse().withStatus(404))); + + makeRequest("GET", "/not-found"); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + Map json = events.get(0).asJson(); + assertEquals("network", json.get("type")); + Map body = (Map) json.get("body"); + assertEquals("404", body.get("status_code")); + assertEquals("GET", body.get("method")); + assertTrue(body.get("url").toString().contains("/not-found")); + } + + @Test + public void serverErrorResponse_recordsNetworkEvent() throws IOException { + server.stubFor(get(urlEqualTo("/error")).willReturn(aResponse().withStatus(500))); + + makeRequest("GET", "/error"); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + Map body = (Map) events.get(0).asJson().get("body"); + assertEquals("500", body.get("status_code")); + } + + @Test + public void redirectResponse_doesNotRecordEvent() throws IOException { + server.stubFor(get(urlEqualTo("/redirect")).willReturn( + aResponse().withStatus(301).withHeader("Location", "/other"))); + + HttpURLConnection conn = (HttpURLConnection) new URL(server.baseUrl() + "/redirect").openConnection(); + conn.setInstanceFollowRedirects(false); + conn.getResponseCode(); + conn.disconnect(); + + assertTrue(AgentTelemetryStore.getInstance().getAll().isEmpty()); + } + + @Test + public void urlSanitization_stripsQueryAndCredentials() throws IOException { + server.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(500))); + + HttpURLConnection conn = (HttpURLConnection) new URL( + server.baseUrl() + "/path?secret=abc&token=xyz" + ).openConnection(); + conn.getResponseCode(); + conn.disconnect(); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + Map body = (Map) events.get(0).asJson().get("body"); + String url = body.get("url").toString(); + assertTrue(url.contains("/path")); + assertFalse(url.contains("secret")); + assertFalse(url.contains("token")); + } + + private void makeRequest(String method, String path) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(server.baseUrl() + path).openConnection(); + conn.setRequestMethod(method); + conn.getResponseCode(); + conn.disconnect(); + } +} diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java new file mode 100644 index 00000000..fa31cafd --- /dev/null +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java @@ -0,0 +1,106 @@ +package com.rollbar.agent.instrumentation; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.rollbar.agent.AgentTelemetryStore; +import com.rollbar.agent.NetworkEventBridge; +import com.rollbar.api.payload.data.TelemetryEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +public class JavaHttpClientInstrumentationTest { + + private WireMockServer server; + private HttpClient client; + + @BeforeEach + public void setUp() { + server = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + server.start(); + client = HttpClient.newHttpClient(); + AgentTelemetryStore.resetForTesting(); + NetworkEventBridge.resetRecordedForTesting(); + } + + @AfterEach + public void tearDown() { + server.stop(); + } + + @Test + public void successResponse_doesNotRecordEvent() throws Exception { + server.stubFor(get(urlEqualTo("/ok")).willReturn(aResponse().withStatus(200))); + + client.send( + HttpRequest.newBuilder(URI.create(server.baseUrl() + "/ok")).GET().build(), + HttpResponse.BodyHandlers.discarding() + ); + + assertTrue(AgentTelemetryStore.getInstance().getAll().isEmpty()); + } + + @Test + public void clientErrorResponse_recordsNetworkEvent() throws Exception { + server.stubFor(get(urlEqualTo("/not-found")).willReturn(aResponse().withStatus(404))); + + client.send( + HttpRequest.newBuilder(URI.create(server.baseUrl() + "/not-found")).GET().build(), + HttpResponse.BodyHandlers.discarding() + ); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + Map json = events.get(0).asJson(); + assertEquals("network", json.get("type")); + Map body = (Map) json.get("body"); + assertEquals("404", body.get("status_code")); + assertEquals("GET", body.get("method")); + assertTrue(body.get("url").toString().contains("/not-found")); + } + + @Test + public void serverErrorResponse_recordsNetworkEvent() throws Exception { + server.stubFor(post(urlEqualTo("/error")).willReturn(aResponse().withStatus(500))); + + client.send( + HttpRequest.newBuilder(URI.create(server.baseUrl() + "/error")) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(), + HttpResponse.BodyHandlers.discarding() + ); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + Map body = (Map) events.get(0).asJson().get("body"); + assertEquals("500", body.get("status_code")); + assertEquals("POST", body.get("method")); + } + + @Test + public void urlSanitization_stripsQuery() throws Exception { + server.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(500))); + + client.send( + HttpRequest.newBuilder(URI.create(server.baseUrl() + "/path?token=secret")).GET().build(), + HttpResponse.BodyHandlers.discarding() + ); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + Map body = (Map) events.get(0).asJson().get("body"); + String url = body.get("url").toString(); + assertTrue(url.contains("/path")); + assertFalse(url.contains("secret")); + } +} From 8ab880c60aa5d9cc0feafc368adb6a39f7e9f98b Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 25 May 2026 17:36:13 -0300 Subject: [PATCH 05/10] docs(java-agent): add README with installation and manual testing guide --- rollbar-java-agent/README.md | 176 +++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 rollbar-java-agent/README.md diff --git a/rollbar-java-agent/README.md b/rollbar-java-agent/README.md new file mode 100644 index 00000000..e37bbbfd --- /dev/null +++ b/rollbar-java-agent/README.md @@ -0,0 +1,176 @@ +# Rollbar Java Agent + +A zero-code-change Java instrumentation agent that automatically captures HTTP network errors (4xx and 5xx responses) as Rollbar telemetry events. + +It works by attaching to the JVM at startup via `-javaagent:` and using ByteBuddy to intercept HTTP calls across all major clients — no library dependencies or code changes are needed in your application. + +## Instrumented HTTP clients + +| Client | Condition | +|--------|-----------| +| `java.net.HttpURLConnection` | Always (JDK built-in) | +| `java.net.http.HttpClient` | Java 11+ only | +| Apache HttpClient 4.x (`org.apache.http`) | If present on classpath | +| Apache HttpClient 5.x (`org.apache.hc.client5`) | If present on classpath | + +Only 4xx and 5xx responses are recorded. Successful requests (< 400) produce no telemetry. + +## Requirements + +- Java 11 or higher +- `rollbar-java` on the application classpath (for `Rollbar.init(...)`) + +## Installation + +### 1. Build the agent JAR + +```bash +./gradlew :rollbar-java-agent:shadowJar +``` + +The fat JAR (with ByteBuddy bundled and relocated) is written to: + +``` +rollbar-java-agent/build/libs/rollbar-java-agent-.jar +``` + +### 2. Add the agent JVM flag + +Add `-javaagent:` to your JVM startup arguments, pointing at the JAR built above: + +``` +-javaagent:/path/to/rollbar-java-agent-.jar +``` + +**Gradle:** +```kotlin +jvmArgs("-javaagent:/path/to/rollbar-java-agent-.jar") +``` + +**Maven Surefire / Failsafe:** +```xml +-javaagent:/path/to/rollbar-java-agent-.jar +``` + +**Docker / environment variable:** +```bash +JAVA_TOOL_OPTIONS="-javaagent:/path/to/rollbar-java-agent-.jar" +``` + +### 3. Also add the JAR to your classpath + +The agent JAR must also be available on the regular application classpath so that your code can call `RollbarAgent.getTelemetryTracker()`: + +**Gradle:** +```kotlin +dependencies { + implementation(files("/path/to/rollbar-java-agent-.jar")) +} +``` + +**Maven:** +```xml + + com.rollbar + rollbar-java-agent + ${rollbar.version} + +``` + +### 4. Wire into your Rollbar configuration + +```java +import com.rollbar.agent.RollbarAgent; +import com.rollbar.notifier.Rollbar; + +import static com.rollbar.notifier.config.ConfigBuilder.withAccessToken; + +Rollbar rollbar = Rollbar.init( + withAccessToken("your-access-token") + .environment("production") + .telemetryEventTracker(RollbarAgent.getTelemetryTracker()) + .build() +); +``` + +That's it. All HTTP calls your application makes from that point on will automatically produce telemetry events in the Rollbar error report for any 4xx or 5xx response. + +## Behavior + +| Scenario | Action | +|----------|--------| +| Response status `< 400` | No telemetry recorded | +| Response status `>= 400` | Records a network telemetry event with `Level.CRITICAL` | +| Connection failure / I/O error | Records an error telemetry event | +| No Rollbar config wired | Events accumulate in the agent store (capacity 100); nothing is sent | + +## Security + +URLs can carry sensitive data in query parameters or basic-auth credentials. The agent **strips userinfo, query parameters, and the URL fragment** before recording. + +For example, a request to: +``` +https://user:secret@api.example.com/charge?token=sk_live_abc#section +``` +is recorded as: +``` +https://api.example.com/charge +``` + +## Testing + +### Automated tests + +```bash +./gradlew :rollbar-java-agent:test +``` + +This runs the full test suite (WireMock-backed integration tests for each instrumented client). + +### Manual smoke test + +1. Build the agent JAR: + ```bash + ./gradlew :rollbar-java-agent:shadowJar + ``` + +2. Write a small program that triggers a 4xx or 5xx: + ```java + import com.rollbar.agent.RollbarAgent; + import com.rollbar.notifier.Rollbar; + + import java.net.HttpURLConnection; + import java.net.URL; + + import static com.rollbar.notifier.config.ConfigBuilder.withAccessToken; + + public class SmokeTest { + public static void main(String[] args) throws Exception { + Rollbar rollbar = Rollbar.init( + withAccessToken("your-access-token") + .environment("test") + .telemetryEventTracker(RollbarAgent.getTelemetryTracker()) + .build() + ); + + // Trigger a 404 — captured as a telemetry event on the next error report + HttpURLConnection conn = (HttpURLConnection) new URL("https://httpstat.us/404").openConnection(); + int code = conn.getResponseCode(); + conn.disconnect(); + + System.out.println("Response: " + code); + + // Send an error to Rollbar — the 404 telemetry event will appear alongside it + rollbar.error(new RuntimeException("smoke test error")); + } + } + ``` + +3. Run with the agent: + ```bash + java -javaagent:rollbar-java-agent/build/libs/rollbar-java-agent-.jar \ + -cp "rollbar-java-agent/build/libs/rollbar-java-agent-.jar:your-app.jar" \ + SmokeTest + ``` + +4. Check your Rollbar dashboard — the error report for "smoke test error" should show a **Network** telemetry event for the 404 in the telemetry timeline. From ad0b2acf09032f3f56078a7031575956e7865cd3 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 26 May 2026 00:24:36 -0300 Subject: [PATCH 06/10] chore(java-agent): upgrade wiremock to org.wiremock:wiremock:3.13.2 --- rollbar-java-agent/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollbar-java-agent/build.gradle.kts b/rollbar-java-agent/build.gradle.kts index 34facdef..dcdc4c7f 100644 --- a/rollbar-java-agent/build.gradle.kts +++ b/rollbar-java-agent/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("org.mockito:mockito-core:5.11.0") - testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") + testImplementation("org.wiremock:wiremock:3.13.2") testImplementation("org.apache.httpcomponents:httpclient:4.5.14") testImplementation("org.apache.httpcomponents.client5:httpclient5:5.3.1") } From 64e7df9b7fcc0b45032ac52d38fcbae0b7d2150a Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 26 May 2026 00:48:50 -0300 Subject: [PATCH 07/10] refactor(java-agent): replace resetForTesting with injectable Provider in AgentTelemetryStore --- .../rollbar/agent/AgentTelemetryStore.java | 5 +++-- .../com/rollbar/agent/RollbarAgentTest.java | 22 ++++++++++++++++++- .../HttpURLConnectionInstrumentationTest.java | 2 +- .../JavaHttpClientInstrumentationTest.java | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java index 35bf036f..cd6d6503 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/AgentTelemetryStore.java @@ -1,6 +1,7 @@ package com.rollbar.agent; import com.rollbar.api.payload.data.TelemetryEvent; +import com.rollbar.notifier.provider.Provider; import com.rollbar.notifier.telemetry.RollbarTelemetryEventTracker; import com.rollbar.notifier.telemetry.TelemetryEventTracker; @@ -21,7 +22,7 @@ public static List getAll() { return INSTANCE.getAll(); } - public static void resetForTesting() { - INSTANCE = new RollbarTelemetryEventTracker(System::currentTimeMillis, 100); + public static void init(Provider timestampProvider) { + INSTANCE = new RollbarTelemetryEventTracker(timestampProvider, 100); } } diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java index 0efbc44c..cc6f82ee 100644 --- a/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/RollbarAgentTest.java @@ -1,16 +1,20 @@ package com.rollbar.agent; +import com.rollbar.api.payload.data.TelemetryEvent; import com.rollbar.notifier.telemetry.TelemetryEventTracker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; + public class RollbarAgentTest { @BeforeEach public void setUp() { - AgentTelemetryStore.resetForTesting(); + AgentTelemetryStore.init(System::currentTimeMillis); } @Test @@ -20,6 +24,22 @@ public void getTelemetryTracker_returnsSingletonInstance() { assertSame(first, second); } + @Test + public void init_withCustomTimestamp_usesProvidedTimestamp() { + long fixedTime = 1_000_000L; + AgentTelemetryStore.init(() -> fixedTime); + + AgentTelemetryStore.getInstance().recordManualEventFor( + com.rollbar.api.payload.data.Level.WARNING, + com.rollbar.api.payload.data.Source.CLIENT, + "test" + ); + + List events = AgentTelemetryStore.getInstance().getAll(); + assertEquals(1, events.size()); + assertEquals(fixedTime, events.get(0).asJson().get("timestamp_ms")); + } + @Test public void urlSanitizer_stripsQueryAndFragment() { String sanitized = UrlSanitizer.sanitize("https://api.example.com/path?token=secret#section"); diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java index 6044e832..03b91345 100644 --- a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java @@ -26,7 +26,7 @@ public class HttpURLConnectionInstrumentationTest { public void setUp() { server = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); server.start(); - AgentTelemetryStore.resetForTesting(); + AgentTelemetryStore.init(System::currentTimeMillis); NetworkEventBridge.resetRecordedForTesting(); } diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java index fa31cafd..785f04fe 100644 --- a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentationTest.java @@ -29,7 +29,7 @@ public void setUp() { server = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); server.start(); client = HttpClient.newHttpClient(); - AgentTelemetryStore.resetForTesting(); + AgentTelemetryStore.init(System::currentTimeMillis); NetworkEventBridge.resetRecordedForTesting(); } From 212ce90e7311cfeb347bdb06f47e421441f5637a Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 26 May 2026 01:07:17 -0300 Subject: [PATCH 08/10] fix(java-agent): resolve checkstyle violations in rollbar-java-agent --- .../com/rollbar/agent/NetworkEventBridge.java | 10 ++++++++ .../java/com/rollbar/agent/RollbarAgent.java | 3 +-- .../ApacheHttpClient4Instrumentation.java | 21 +++++++++++++--- .../ApacheHttpClient5Instrumentation.java | 22 ++++++++++++++--- .../HttpURLConnectionInstrumentation.java | 24 +++++++++++++++---- .../JavaHttpClientInstrumentation.java | 24 +++++++++++++++---- 6 files changed, 86 insertions(+), 18 deletions(-) diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java index 947c5b34..332bcffb 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/NetworkEventBridge.java @@ -37,6 +37,11 @@ public static boolean markAsRecorded(Object key) { return RECORDED.add(key); } + /** + * Records a network telemetry event for the given key if not already recorded. + * + *

Uses the key as a deduplication token — subsequent calls with the same key are ignored. + */ public static void recordNetworkEvent(Object key, String method, String url, String statusCode) { if (!markAsRecorded(key)) { return; // deduplicate re-entrant calls for the same connection @@ -50,6 +55,11 @@ public static void recordNetworkEvent(Object key, String method, String url, Str ); } + /** + * Records a manual error telemetry event with the given message. + * + *

Called when an HTTP request fails with an I/O exception rather than a status code. + */ public static void recordError(String message) { AgentTelemetryStore.getInstance().recordManualEventFor( Level.CRITICAL, diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java index 8eb04d68..fc3ef476 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java @@ -5,11 +5,10 @@ import com.rollbar.agent.instrumentation.HttpURLConnectionInstrumentation; import com.rollbar.agent.instrumentation.JavaHttpClientInstrumentation; import com.rollbar.notifier.telemetry.TelemetryEventTracker; +import java.lang.instrument.Instrumentation; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.matcher.ElementMatchers; -import java.lang.instrument.Instrumentation; - /** * Java agent entry point. Attach with {@code -javaagent:/path/to/rollbar-java-agent.jar}. * diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java index be57fabf..6a86d86e 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient4Instrumentation.java @@ -4,18 +4,25 @@ import com.rollbar.agent.UrlSanitizer; import com.rollbar.api.payload.data.Level; import com.rollbar.api.payload.data.Source; +import java.lang.instrument.Instrumentation; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpUriRequest; -import java.lang.instrument.Instrumentation; - +/** + * Installs ByteBuddy advice on Apache HttpClient 4.x to capture network errors. + */ public final class ApacheHttpClient4Instrumentation { private ApacheHttpClient4Instrumentation() {} + /** + * Instruments Apache HttpClient 4.x if present on the classpath. + * + *

Does nothing if {@code org.apache.http.impl.client.CloseableHttpClient} is not available. + */ public static void installIfAvailable(AgentBuilder builder, Instrumentation inst) { try { Class.forName("org.apache.http.impl.client.CloseableHttpClient"); @@ -41,6 +48,12 @@ public static void installIfAvailable(AgentBuilder builder, Instrumentation inst */ public static class ExecuteAdvice { + /** + * Fires after {@code execute()} returns or throws, recording 4xx/5xx responses as telemetry. + * + *

Apache HC 4.x runs in the application classloader, so Rollbar classes are referenced + * directly without the TCCL reflection bridge needed for JDK instrumentation. + */ @Advice.OnMethodExit(onThrowable = Throwable.class) public static void onExit( @Advice.Argument(0) HttpUriRequest request, @@ -49,10 +62,12 @@ public static void onExit( ) { try { if (thrown != null) { + String message = thrown.getMessage() != null + ? thrown.getMessage() : thrown.getClass().getName(); AgentTelemetryStore.getInstance().recordManualEventFor( Level.CRITICAL, Source.SERVER, - "Network error: " + (thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName()) + "Network error: " + message ); return; } diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java index 95d6e8c4..d2aaf927 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/ApacheHttpClient5Instrumentation.java @@ -4,18 +4,26 @@ import com.rollbar.agent.UrlSanitizer; import com.rollbar.api.payload.data.Level; import com.rollbar.api.payload.data.Source; +import java.lang.instrument.Instrumentation; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; -import java.lang.instrument.Instrumentation; - +/** + * Installs ByteBuddy advice on Apache HttpClient 5.x to capture network errors. + */ public final class ApacheHttpClient5Instrumentation { private ApacheHttpClient5Instrumentation() {} + /** + * Instruments Apache HttpClient 5.x if present on the classpath. + * + *

Does nothing if {@code org.apache.hc.client5.http.impl.classic.CloseableHttpClient} + * is not available. + */ public static void installIfAvailable(AgentBuilder builder, Instrumentation inst) { try { Class.forName("org.apache.hc.client5.http.impl.classic.CloseableHttpClient"); @@ -41,6 +49,12 @@ public static void installIfAvailable(AgentBuilder builder, Instrumentation inst */ public static class ExecuteAdvice { + /** + * Fires after {@code execute()} returns or throws, recording 4xx/5xx responses as telemetry. + * + *

Apache HC 5.x runs in the application classloader, so Rollbar classes are referenced + * directly without the TCCL reflection bridge needed for JDK instrumentation. + */ @Advice.OnMethodExit(onThrowable = Throwable.class) public static void onExit( @Advice.Argument(0) ClassicHttpRequest request, @@ -49,10 +63,12 @@ public static void onExit( ) { try { if (thrown != null) { + String message = thrown.getMessage() != null + ? thrown.getMessage() : thrown.getClass().getName(); AgentTelemetryStore.getInstance().recordManualEventFor( Level.CRITICAL, Source.SERVER, - "Network error: " + (thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName()) + "Network error: " + message ); return; } diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java index b2a8c513..3f3e1447 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java @@ -1,15 +1,22 @@ package com.rollbar.agent.instrumentation; +import java.lang.instrument.Instrumentation; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; -import java.lang.instrument.Instrumentation; - +/** + * Installs ByteBuddy advice on {@code java.net.HttpURLConnection} to capture network errors. + */ public final class HttpURLConnectionInstrumentation { private HttpURLConnectionInstrumentation() {} + /** + * Instruments {@code java.net.HttpURLConnection.getResponseCode()} to record 4xx/5xx responses. + * + *

Targets the declaring class directly to capture the method regardless of concrete subtype. + */ public static void install(AgentBuilder builder, Instrumentation inst) { builder .type(ElementMatchers.named("java.net.HttpURLConnection")) @@ -29,6 +36,12 @@ public static void install(AgentBuilder builder, Instrumentation inst) { */ public static class GetResponseCodeAdvice { + /** + * Fires after {@code getResponseCode()} returns or throws, recording 4xx/5xx as telemetry. + * + *

Uses the connection instance as a deduplication key since {@code getResponseCode()} is + * called re-entrantly up to 3 times per request internally. + */ @Advice.OnMethodExit(onThrowable = Throwable.class) public static void onExit( @Advice.This Object connection, @@ -46,7 +59,8 @@ public static void onExit( Boolean recorded = (Boolean) bridge .getMethod("markAsRecorded", Object.class).invoke(null, thrown); if (recorded) { - String msg = thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName(); + String msg = thrown.getMessage() != null + ? thrown.getMessage() : thrown.getClass().getName(); bridge.getMethod("recordError", String.class).invoke(null, msg); } return; @@ -55,8 +69,8 @@ public static void onExit( if (statusCode >= 400) { Object url = connection.getClass().getMethod("getURL").invoke(connection); String urlStr = url != null ? url.toString() : ""; - String method = (String) connection.getClass().getMethod("getRequestMethod").invoke(connection); - // connection instance as dedup key — same object across all re-entrant getResponseCode() calls + String method = (String) connection.getClass() + .getMethod("getRequestMethod").invoke(connection); bridge.getMethod("recordNetworkEvent", Object.class, String.class, String.class, String.class) .invoke(null, connection, method, urlStr, String.valueOf(statusCode)); diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java index 5247f832..5f15a92c 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/JavaHttpClientInstrumentation.java @@ -1,16 +1,23 @@ package com.rollbar.agent.instrumentation; +import java.lang.instrument.Instrumentation; +import java.net.http.HttpClient; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; -import java.lang.instrument.Instrumentation; -import java.net.http.HttpClient; - +/** + * Installs ByteBuddy advice on {@code java.net.http.HttpClient} subtypes to capture network errors. + */ public final class JavaHttpClientInstrumentation { private JavaHttpClientInstrumentation() {} + /** + * Instruments {@code java.net.http.HttpClient} subtypes if available on the current JVM. + * + *

Does nothing if {@code java.net.http.HttpClient} is not present (i.e. below Java 11). + */ public static void installIfAvailable(AgentBuilder builder, Instrumentation inst) { try { Class.forName("java.net.http.HttpClient"); @@ -37,6 +44,12 @@ public static void installIfAvailable(AgentBuilder builder, Instrumentation inst */ public static class SendAdvice { + /** + * Fires after {@code send()} returns or throws, recording 4xx/5xx responses as telemetry. + * + *

Uses the response object as a deduplication key to avoid duplicate events when both + * {@code HttpClientFacade} and {@code HttpClientImpl} invoke this advice. + */ @Advice.OnMethodExit(onThrowable = Throwable.class) public static void onExit( @Advice.Argument(0) Object request, @@ -54,8 +67,9 @@ public static void onExit( Boolean recorded = (Boolean) bridge .getMethod("markAsRecorded", Object.class).invoke(null, thrown); if (recorded) { - String msg = thrown.getMessage() != null ? thrown.getMessage() : thrown.getClass().getName(); - bridge.getMethod("recordError", String.class).invoke(null, msg); + String message = thrown.getMessage() != null + ? thrown.getMessage() : thrown.getClass().getName(); + bridge.getMethod("recordError", String.class).invoke(null, message); } return; } From fe91c9af99f5a8ee63df54b275f6d5b2f058a032 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 26 May 2026 01:20:36 -0300 Subject: [PATCH 09/10] fix(java-agent): resolve checkstyle violations in rollbar-java-agent --- .../src/main/java/com/rollbar/agent/RollbarAgent.java | 4 ++-- ...rumentation.java => HttpUrlConnectionInstrumentation.java} | 4 ++-- ...ionTest.java => HttpUrlConnectionInstrumentationTest.java} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/{HttpURLConnectionInstrumentation.java => HttpUrlConnectionInstrumentation.java} (96%) rename rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/{HttpURLConnectionInstrumentationTest.java => HttpUrlConnectionInstrumentationTest.java} (98%) diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java index fc3ef476..6377bb11 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/RollbarAgent.java @@ -2,7 +2,7 @@ import com.rollbar.agent.instrumentation.ApacheHttpClient4Instrumentation; import com.rollbar.agent.instrumentation.ApacheHttpClient5Instrumentation; -import com.rollbar.agent.instrumentation.HttpURLConnectionInstrumentation; +import com.rollbar.agent.instrumentation.HttpUrlConnectionInstrumentation; import com.rollbar.agent.instrumentation.JavaHttpClientInstrumentation; import com.rollbar.notifier.telemetry.TelemetryEventTracker; import java.lang.instrument.Instrumentation; @@ -41,7 +41,7 @@ private static void installInstrumentation(Instrumentation inst) { .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .with(AgentBuilder.TypeStrategy.Default.REDEFINE); - HttpURLConnectionInstrumentation.install(builder, inst); + HttpUrlConnectionInstrumentation.install(builder, inst); JavaHttpClientInstrumentation.installIfAvailable(builder, inst); ApacheHttpClient4Instrumentation.installIfAvailable(builder, inst); ApacheHttpClient5Instrumentation.installIfAvailable(builder, inst); diff --git a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpUrlConnectionInstrumentation.java similarity index 96% rename from rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java rename to rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpUrlConnectionInstrumentation.java index 3f3e1447..5ee10ec6 100644 --- a/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentation.java +++ b/rollbar-java-agent/src/main/java/com/rollbar/agent/instrumentation/HttpUrlConnectionInstrumentation.java @@ -8,9 +8,9 @@ /** * Installs ByteBuddy advice on {@code java.net.HttpURLConnection} to capture network errors. */ -public final class HttpURLConnectionInstrumentation { +public final class HttpUrlConnectionInstrumentation { - private HttpURLConnectionInstrumentation() {} + private HttpUrlConnectionInstrumentation() {} /** * Instruments {@code java.net.HttpURLConnection.getResponseCode()} to record 4xx/5xx responses. diff --git a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpUrlConnectionInstrumentationTest.java similarity index 98% rename from rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java rename to rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpUrlConnectionInstrumentationTest.java index 03b91345..319556ad 100644 --- a/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpURLConnectionInstrumentationTest.java +++ b/rollbar-java-agent/src/test/java/com/rollbar/agent/instrumentation/HttpUrlConnectionInstrumentationTest.java @@ -18,7 +18,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.junit.jupiter.api.Assertions.*; -public class HttpURLConnectionInstrumentationTest { +public class HttpUrlConnectionInstrumentationTest { private WireMockServer server; From 866b20b7ebb5821953761c368c28611ab55cf67b Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 26 May 2026 02:15:18 -0300 Subject: [PATCH 10/10] fix(rollbar-java-agent): declare explicit dependency on jar task in test --- rollbar-java-agent/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/rollbar-java-agent/build.gradle.kts b/rollbar-java-agent/build.gradle.kts index dcdc4c7f..ff1119ad 100644 --- a/rollbar-java-agent/build.gradle.kts +++ b/rollbar-java-agent/build.gradle.kts @@ -52,4 +52,5 @@ tasks.test { // system classloader; mirrors production use where rollbar-java-agent is a Gradle/Maven dep classpath += files(agentJar) dependsOn(tasks.shadowJar) + dependsOn(tasks.jar) }