Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e19ecf3
feat(okhttp): add telemetry interceptor
buongarzoni Mar 23, 2026
e141e7e
build(okhttp): update dependencies
buongarzoni Mar 24, 2026
ab26f42
chore(okhttp): add readme
buongarzoni Mar 24, 2026
ed9af20
chore(okhttp): fix lint
buongarzoni Mar 24, 2026
4f2e019
fix(okhttp): isolate NetworkTelemetryRecorder failures in interceptor
buongarzoni Apr 20, 2026
e7dd96f
build(okhttp): remove hardcoded version to inherit from root
buongarzoni Apr 20, 2026
ed91c57
build(okhttp): remove redundant plugins and repositories blocks
buongarzoni Apr 20, 2026
28807a1
fix(okhttp): strip query params from recorded URLs by default to prev…
buongarzoni Apr 27, 2026
02c7ea0
fix(okhttp): strip query params from recorded URLs by default to prev…
buongarzoni Apr 27, 2026
fd321cb
fix(okhttp): lint error, decrease line length
buongarzoni Apr 27, 2026
b4db532
fix(okhttp): attribute sanitizer exceptions to urlSanitizer in logs
buongarzoni May 1, 2026
ce27e7b
fix(okhttp): strip credentials and fragment from URLs in default sani…
buongarzoni May 1, 2026
2c505c1
fix(okhttp): replace JUL logger with SLF4J to match SDK conventions
buongarzoni May 1, 2026
3c4f505
build(okhttp): remove redundant mockito-core declaration
buongarzoni May 1, 2026
1d7dab4
docs(okhttp): add rollbar-java to installation snippet
buongarzoni May 1, 2026
f14a9eb
fix(okhttp): lint line length
buongarzoni May 1, 2026
0d75c63
style(okhttp): make test class public and use 2-space indentation
buongarzoni May 1, 2026
5835a82
style: add missing colon prefix to rollbar-okhttp in settings.gradle.kts
buongarzoni May 1, 2026
8af01d0
docs(okhttp): update sanitizer docs to list all stripped URL components
buongarzoni May 1, 2026
be12a54
fix(okhttp): replace java.util.function.Function with custom UrlSanit…
buongarzoni May 2, 2026
106f26d
fix(okhttp): remove incorrect group override so module publishes as c…
buongarzoni May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions rollbar-okhttp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Rollbar OkHttp Integration

This module provides an [OkHttp Interceptor](https://square.github.io/okhttp/features/interceptors/) that automatically captures network telemetry for the Rollbar Java SDK.

It records:

- **Network telemetry events** for HTTP responses with status code `>= 400` (client and server errors).
- **Error events** for connection failures, timeouts, and other I/O exceptions.

## Installation

### Gradle (Kotlin DSL)

```kotlin
dependencies {
implementation("com.rollbar:rollbar-okhttp:<version>")
implementation("com.squareup.okhttp3:okhttp:<okhttp-version>")
}
```

### Gradle (Groovy)

```groovy
dependencies {
implementation 'com.rollbar:rollbar-okhttp:<version>'
implementation 'com.squareup.okhttp3:okhttp:<okhttp-version>'
}
```

## Usage
Comment thread
buongarzoni marked this conversation as resolved.

### 1. Implement `NetworkTelemetryRecorder`

```java
NetworkTelemetryRecorder recorder = new NetworkTelemetryRecorder() {
@Override
public void recordNetworkEvent(Level level, String method, String url, String statusCode) {
rollbar.recordNetworkEventFor(level, method, url, statusCode);
}

@Override
public void recordErrorEvent(Exception exception) {
rollbar.log(exception);
}
};
```

### 2. Add the interceptor to your OkHttpClient

```java
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new RollbarOkHttpInterceptor(recorder))
.build();
```

### 3. Make requests as usual

```java
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();

Response response = client.newCall(request).execute();
```

The interceptor will automatically record telemetry events to Rollbar without interfering with the request/response flow.

## Behavior

| Scenario | Action |
|-----------------------------------|---------------------------------------------------------|
| Recorder is `null` | No telemetry or log is recorded |
| Response status `< 400` | No telemetry recorded, response returned normally |
| Response status `>= 400` | Records a network telemetry event with `Level.CRITICAL` |
| Connection failure / timeout | Records an error event, then rethrows the `IOException` |
23 changes: 23 additions & 0 deletions rollbar-okhttp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id("java")
}
Comment thread
buongarzoni marked this conversation as resolved.
Outdated

group = "com.rollbar.okhttp"
Comment thread
buongarzoni marked this conversation as resolved.
Outdated
Comment thread
brianr marked this conversation as resolved.
Outdated

repositories {
mavenCentral()
}

dependencies {
testImplementation(platform("org.junit:junit-bom:5.14.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
testImplementation("org.mockito:mockito-core:5.23.0")
Comment thread
buongarzoni marked this conversation as resolved.
Outdated
implementation("com.squareup.okhttp3:okhttp:5.3.2")
Comment thread
buongarzoni marked this conversation as resolved.
api(project(":rollbar-api"))
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.rollbar.okhttp;

import com.rollbar.api.payload.data.Level;

public interface NetworkTelemetryRecorder {
void recordNetworkEvent(Level level, String method, String url, String statusCode);

void recordErrorEvent(Exception exception);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.rollbar.okhttp;

import com.rollbar.api.payload.data.Level;

import java.io.IOException;
import java.util.logging.Logger;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class RollbarOkHttpInterceptor implements Interceptor {

private static final Logger LOGGER = Logger.getLogger(RollbarOkHttpInterceptor.class.getName());

private final NetworkTelemetryRecorder recorder;

public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) {
this.recorder = recorder;
}

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();

try {
Response response = chain.proceed(request);

if (response.code() >= 400 && recorder != null) {
try {
recorder.recordNetworkEvent(
Level.CRITICAL,
request.method(),
request.url().toString(),
Comment thread
buongarzoni marked this conversation as resolved.
Outdated
String.valueOf(response.code()));
Comment thread
claude[bot] marked this conversation as resolved.
} catch (Exception recorderException) {
LOGGER.log(java.util.logging.Level.WARNING,
"NetworkTelemetryRecorder.recordNetworkEvent threw an exception; "
+ "suppressing to preserve the interceptor contract.",
recorderException);
}
Comment thread
buongarzoni marked this conversation as resolved.
}

return response;

} catch (IOException e) {
if (recorder != null) {
try {
recorder.recordErrorEvent(e);
} catch (Exception recorderException) {
LOGGER.log(java.util.logging.Level.WARNING,
"NetworkTelemetryRecorder.recordErrorEvent threw an exception; "
+ "suppressing to preserve the original IOException.",
recorderException);
}
}

throw e;
Comment thread
claude[bot] marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.rollbar.okhttp;

import com.rollbar.api.payload.data.Level;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.SocketPolicy;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class RollbarOkHttpInterceptorTest {
Comment thread
buongarzoni marked this conversation as resolved.
Outdated

private MockWebServer server;
private NetworkTelemetryRecorder recorder;
private OkHttpClient client;

@BeforeEach
void setUp() throws IOException {
server = new MockWebServer();
server.start();

recorder = mock(NetworkTelemetryRecorder.class);

client = new OkHttpClient.Builder()
.addInterceptor(new RollbarOkHttpInterceptor(recorder))
.build();
}

@AfterEach
void tearDown() throws IOException {
server.shutdown();
}

@Test
void successfulResponse_doesNotRecordEvent() throws IOException {
server.enqueue(new MockResponse().setResponseCode(200));

Request request = new Request.Builder().url(server.url("/ok")).build();
Response response = client.newCall(request).execute();
response.close();

assertEquals(200, response.code());
verifyNoInteractions(recorder);
}

@Test
void redirectResponse_doesNotRecordEvent() throws IOException {
server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location", "/other"));

OkHttpClient noFollowClient = client.newBuilder().followRedirects(false).build();
Request request = new Request.Builder().url(server.url("/redirect")).build();
Response response = noFollowClient.newCall(request).execute();
response.close();

assertEquals(301, response.code());
verifyNoInteractions(recorder);
}

@Test
void clientErrorResponse_recordsNetworkEvent() throws IOException {
server.enqueue(new MockResponse().setResponseCode(404));

Request request = new Request.Builder().url(server.url("/not-found")).build();
Response response = client.newCall(request).execute();
response.close();

assertEquals(404, response.code());
verify(recorder).recordNetworkEvent(
eq(Level.CRITICAL), eq("GET"), contains("/not-found"), eq("404"));
verify(recorder, never()).recordErrorEvent(any());
}

@Test
void serverErrorResponse_recordsNetworkEvent() throws IOException {
server.enqueue(new MockResponse().setResponseCode(500));

Request request = new Request.Builder().url(server.url("/error")).build();
Response response = client.newCall(request).execute();
response.close();

assertEquals(500, response.code());
verify(recorder).recordNetworkEvent(
eq(Level.CRITICAL), eq("GET"), contains("/error"), eq("500"));
verify(recorder, never()).recordErrorEvent(any());
}

@Test
void connectionFailure_recordsErrorEvent() {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));

Request request = new Request.Builder().url(server.url("/fail")).build();

assertThrows(IOException.class, () -> client.newCall(request).execute());

verify(recorder).recordErrorEvent(any(IOException.class));
verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any());
}

@Test
void postRequest_recordsCorrectMethod() throws IOException {
server.enqueue(new MockResponse().setResponseCode(500));

Request request = new Request.Builder()
.url(server.url("/post"))
.post(okhttp3.RequestBody.create("body", okhttp3.MediaType.parse("text/plain")))
.build();
Response response = client.newCall(request).execute();
response.close();

verify(recorder).recordNetworkEvent(eq(Level.CRITICAL), eq("POST"), any(), eq("500"));
}

@Test
void nullRecorder_errorResponse_doesNotThrowNPE() throws IOException {
server.enqueue(new MockResponse().setResponseCode(500));

OkHttpClient nullRecorderClient = new OkHttpClient.Builder()
.addInterceptor(new RollbarOkHttpInterceptor(null))
.build();

Request request = new Request.Builder().url(server.url("/error")).build();
Response response = nullRecorderClient.newCall(request).execute();
response.close();

assertEquals(500, response.code());
}

@Test
void nullRecorder_connectionFailure_doesNotThrow() {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));

OkHttpClient nullRecorderClient = new OkHttpClient.Builder()
.addInterceptor(new RollbarOkHttpInterceptor(null))
.build();

Request request = new Request.Builder().url(server.url("/fail")).build();

assertThrows(IOException.class, () -> nullRecorderClient.newCall(request).execute());
}

@Test
void recorderThrowsOnErrorResponse_responseStillReturned() throws IOException {
server.enqueue(new MockResponse().setResponseCode(500));

doThrow(new RuntimeException("recorder boom"))
.when(recorder)
.recordNetworkEvent(any(), any(), any(), any());

Request request = new Request.Builder().url(server.url("/error")).build();
Response response = client.newCall(request).execute();
response.close();

assertEquals(500, response.code());
}

@Test
void recorderThrowsOnConnectionFailure_originalIOExceptionPropagates() {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));

doThrow(new RuntimeException("recorder boom"))
.when(recorder)
.recordErrorEvent(any());

Request request = new Request.Builder().url(server.url("/fail")).build();

assertThrows(IOException.class, () -> client.newCall(request).execute());
verify(recorder).recordErrorEvent(any(IOException.class));
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ include(
":rollbar-jakarta-web",
":rollbar-log4j2",
":rollbar-logback",
"rollbar-okhttp",
Comment thread
buongarzoni marked this conversation as resolved.
Outdated
":rollbar-spring-webmvc",
":rollbar-spring6-webmvc",
":rollbar-spring-boot-webmvc",
Expand Down
Loading