-
Notifications
You must be signed in to change notification settings - Fork 74
[Feat] [SDK-399] Okhttp interceptor #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e19ecf3
e141e7e
ab26f42
ed9af20
4f2e019
e7dd96f
ed91c57
28807a1
02c7ea0
fd321cb
b4db532
ce27e7b
2c505c1
3c4f505
1d7dab4
f14a9eb
0d75c63
5835a82
8af01d0
be12a54
106f26d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # 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-java:<version>") | ||
| implementation("com.rollbar:rollbar-okhttp:<version>") | ||
| implementation("com.squareup.okhttp3:okhttp:<okhttp-version>") | ||
| } | ||
| ``` | ||
|
|
||
| ### Gradle (Groovy) | ||
|
|
||
| ```groovy | ||
| dependencies { | ||
| implementation 'com.rollbar:rollbar-java:<version>' | ||
| implementation 'com.rollbar:rollbar-okhttp:<version>' | ||
| implementation 'com.squareup.okhttp3:okhttp:<okhttp-version>' | ||
| } | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### 1. Implement `NetworkTelemetryRecorder` | ||
|
|
||
| ```java | ||
| NetworkTelemetryRecorder recorder = new NetworkTelemetryRecorder() { | ||
| @Override | ||
| public void recordNetworkEvent(Level level, String method, String url, String statusCode) { | ||
| // url has userinfo, query parameters, and fragment stripped by default | ||
| // (see Security section below) | ||
| 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` | | ||
|
|
||
| ## Security | ||
|
|
||
| URLs can carry sensitive data in several components. To prevent accidental leakage to Rollbar, the interceptor **strips userinfo (basic-auth credentials), query parameters, and the fragment by default** before passing the URL to `NetworkTelemetryRecorder`. | ||
|
|
||
| For example, a request to `https://user:secret@api.example.com/charge?token=sk_live_secret#section` will be recorded as `https://api.example.com/charge`. | ||
|
|
||
| If your URLs do not contain sensitive query parameters and you need them for debugging, you can opt in to the full URL by supplying a custom sanitizer: | ||
|
|
||
| ```java | ||
| OkHttpClient client = new OkHttpClient.Builder() | ||
| .addInterceptor(new RollbarOkHttpInterceptor(recorder, HttpUrl::toString)) | ||
| .build(); | ||
| ``` | ||
|
|
||
| When using a custom sanitizer, you are responsible for ensuring that sensitive query parameters are removed before the URL reaches Rollbar. | ||
|
buongarzoni marked this conversation as resolved.
Comment on lines
+87
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The opt-in example at README.md:91 uses Extended reasoning...What is wrong The Security section frames URL sanitization as a defense for three sensitive URL components and the default sanitizer at But the immediately following opt-in subsection narrows the framing back to one component:
Step-by-step proof of the silent credential leak
The reader was never warned about userinfo or fragments because lines 87 and 95 only flag query parameters. Why this PR is the right place to fix it Commit 8af01d0 ( Suggested fix — either of these:
Severity rationale Docs-only, opt-in is deliberate, and URL-embedded basic-auth is rare in modern API usage — so this is a |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 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") | ||
| implementation("com.squareup.okhttp3:okhttp:5.3.2") | ||
|
buongarzoni marked this conversation as resolved.
|
||
| api(project(":rollbar-api")) | ||
| api("org.slf4j:slf4j-api:1.7.25") | ||
| } | ||
|
|
||
| tasks.test { | ||
| useJUnitPlatform() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.rollbar.okhttp; | ||
|
|
||
| import com.rollbar.api.payload.data.Level; | ||
|
|
||
| /** | ||
| * Records network telemetry events and errors for HTTP requests. | ||
| */ | ||
| public interface NetworkTelemetryRecorder { | ||
| /** | ||
| * Records a completed network request as a telemetry event. | ||
| * | ||
| * @param level the severity level to attach to the telemetry event | ||
| * @param method the HTTP method (e.g. GET, POST) | ||
| * @param url the request URL with userinfo (basic-auth credentials), query parameters, | ||
| * and fragment stripped by default; supply a custom sanitizer to | ||
| * {@link RollbarOkHttpInterceptor} to change this behavior | ||
| * @param statusCode the HTTP response status code as a string (e.g. "200", "404") | ||
| */ | ||
| void recordNetworkEvent(Level level, String method, String url, String statusCode); | ||
|
|
||
| /** | ||
| * Records a network error event when an HTTP request fails with an exception. | ||
| * | ||
| * @param exception the exception thrown during the request | ||
| */ | ||
| void recordErrorEvent(Exception exception); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package com.rollbar.okhttp; | ||
|
|
||
| import com.rollbar.api.payload.data.Level; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Objects; | ||
|
|
||
| import okhttp3.Interceptor; | ||
| import okhttp3.Request; | ||
| import okhttp3.Response; | ||
|
|
||
| import org.jetbrains.annotations.NotNull; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| public class RollbarOkHttpInterceptor implements Interceptor { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(RollbarOkHttpInterceptor.class); | ||
|
|
||
| private static final UrlSanitizer DEFAULT_URL_SANITIZER = | ||
| url -> url | ||
| .newBuilder() | ||
| .username("") | ||
| .password("") | ||
| .query(null) | ||
| .fragment(null) | ||
| .build() | ||
| .toString(); | ||
|
|
||
| private final NetworkTelemetryRecorder recorder; | ||
| private final UrlSanitizer urlSanitizer; | ||
|
|
||
| public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { | ||
| this(recorder, DEFAULT_URL_SANITIZER); | ||
| } | ||
|
|
||
| public RollbarOkHttpInterceptor( | ||
| NetworkTelemetryRecorder recorder, | ||
| UrlSanitizer urlSanitizer) { | ||
| this.recorder = recorder; | ||
| this.urlSanitizer = Objects.requireNonNull(urlSanitizer, "urlSanitizer must not be null"); | ||
| } | ||
|
|
||
| @NotNull | ||
| @Override | ||
| public Response intercept(Chain chain) throws IOException { | ||
| Request request = chain.request(); | ||
|
|
||
| try { | ||
| Response response = chain.proceed(request); | ||
|
|
||
| if (response.code() >= 400 && recorder != null) { | ||
| String sanitizedUrl; | ||
| try { | ||
| sanitizedUrl = urlSanitizer.sanitize(request.url()); | ||
| } catch (Exception sanitizerException) { | ||
| LOGGER.warn("urlSanitizer threw an exception; " | ||
| + "suppressing to preserve the interceptor contract.", sanitizerException); | ||
| return response; | ||
| } | ||
| try { | ||
| recorder.recordNetworkEvent( | ||
| Level.CRITICAL, | ||
| request.method(), | ||
| sanitizedUrl, | ||
| String.valueOf(response.code())); | ||
|
claude[bot] marked this conversation as resolved.
|
||
| } catch (Exception recorderException) { | ||
| LOGGER.warn("NetworkTelemetryRecorder.recordNetworkEvent threw an exception; " | ||
| + "suppressing to preserve the interceptor contract.", recorderException); | ||
| } | ||
|
buongarzoni marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return response; | ||
|
|
||
| } catch (IOException e) { | ||
| if (recorder != null) { | ||
| try { | ||
| recorder.recordErrorEvent(e); | ||
| } catch (Exception recorderException) { | ||
| LOGGER.warn("NetworkTelemetryRecorder.recordErrorEvent threw an exception; " | ||
| + "suppressing to preserve the original IOException.", recorderException); | ||
| } | ||
| } | ||
|
|
||
| throw e; | ||
|
claude[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.rollbar.okhttp; | ||
|
|
||
| import okhttp3.HttpUrl; | ||
|
|
||
| @FunctionalInterface | ||
| public interface UrlSanitizer { | ||
| String sanitize(HttpUrl url); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.