Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.8.0] - Unreleased

### Fixed

- `ApiClient` no longer leaks OkHttp's default `User-Agent` header (`okhttp/<version>`) on outbound requests. Requests now omit the `User-Agent` header entirely unless explicitly set by the caller. This resolves cases where mParticle was enriching `device_info.http_header_user_agent` with a meaningless library-identifier string. Caller-supplied values (set via a custom OkHttp interceptor) are preserved. See PR notes for version bump guidance.

## [2.7.0] - 2026-03-17

### Added
Expand Down Expand Up @@ -69,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Don't remove attributes when value is set to null

[2.8.0]: https://github.com/mParticle/mparticle-java-events-sdk/compare/v2.7.0...v2.8.0
[2.7.0]: https://github.com/mParticle/mparticle-java-events-sdk/compare/v2.6.0...v2.7.0
[2.6.0]: https://github.com/mParticle/mparticle-java-events-sdk/compare/v2.5.4...v2.6.0
[2.5.4]: https://github.com/mParticle/mparticle-java-events-sdk/compare/v2.5.3...v2.5.4
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ Call<Void> singleResult = api.uploadEvents(batch);
Response<Void> singleResponse = singleResult.execute();
```

### User-Agent Header

The SDK does not set a `User-Agent` header on outbound requests. OkHttp's default `User-Agent` (`okhttp/<version>`) is explicitly suppressed to prevent mParticle from enriching `device_info.http_header_user_agent` with a meaningless library-identifier string.

If you need to send a custom `User-Agent`, add an OkHttp interceptor that sets the header:

```java
EventsApi api = new ApiClient("API KEY", "API-SECRET") {{
getOkBuilder().addInterceptor(chain -> {
okhttp3.Request request = chain.request().newBuilder()
.header("User-Agent", "my-app/1.0")
.build();
return chain.proceed(request);
});
}}.createService(EventsApi.class);
```

The SDK's suppression only targets OkHttp's own default (`okhttp/<version>`); any other value is passed through unchanged.

### Logging

By default, logging is ignored. Please implement your own LogHandler to handle log statements.
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
api "io.gsonfire:gson-fire:$json_fire_version"
api "org.threeten:threetenbp:$threetenbp_version"
testImplementation "junit:junit:$junit_version"
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.9"
testImplementation "ch.qos.logback:logback-core:$slf4j_core_version"
testImplementation "ch.qos.logback:logback-classic:$slf4j_classic_version"
testImplementation "org.slf4j:slf4j-api:$slf4j_version"
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/mparticle/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
Expand Down Expand Up @@ -47,6 +48,7 @@ public ApiClient(String apiKey, String apiSecret) {
public void createDefaultAdapter() {
json = new JSON();
okBuilder = new OkHttpClient.Builder();
okBuilder.addNetworkInterceptor(new UserAgentInterceptor());

String baseUrl = "https://s2s.mparticle.com/v2";
if (!baseUrl.endsWith("/"))
Expand Down Expand Up @@ -137,6 +139,25 @@ public void addAuthsToOkBuilder(OkHttpClient.Builder okBuilder) {
public void configureFromOkclient(OkHttpClient okClient) {
this.okBuilder = okClient.newBuilder();
addAuthsToOkBuilder(this.okBuilder);
this.okBuilder.addNetworkInterceptor(new UserAgentInterceptor());
}

// Applied as a network interceptor so it runs after OkHttp's BridgeInterceptor,
// which would otherwise inject "okhttp/<version>" whenever User-Agent is absent.
// Caller-supplied values (not starting with "okhttp/") are passed through unchanged.
static class UserAgentInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
String ua = original.header("User-Agent");
if (ua != null && ua.startsWith("okhttp/")) {
Request stripped = original.newBuilder()
.removeHeader("User-Agent")
.build();
return chain.proceed(stripped);
}
return chain.proceed(original);
}
}

static class RateLimitInterceptor implements Interceptor {
Expand Down
97 changes: 97 additions & 0 deletions src/test/java/com/mparticle/client/ApiClientUserAgentTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.mparticle.client;

import com.mparticle.ApiClient;
import com.mparticle.model.Batch;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import retrofit2.Call;

import java.io.IOException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

public class ApiClientUserAgentTest {

private MockWebServer server;

@Before
public void setUp() throws IOException {
server = new MockWebServer();
server.start();
}

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

private EventsApi buildApi(ApiClient client) {
client.getAdapterBuilder().baseUrl(server.url("/"));
return client.createService(EventsApi.class);
}

// Case 1: Default path — OkHttp's User-Agent must not appear on the wire.
@Test
public void defaultPath_noUserAgentHeader() throws Exception {
server.enqueue(new MockResponse().setResponseCode(202));

EventsApi api = buildApi(new ApiClient("key", "secret"));
Call<Void> call = api.uploadEvents(new Batch().environment(Batch.Environment.DEVELOPMENT));
call.execute();

RecordedRequest recorded = server.takeRequest();
assertNull("Expected no User-Agent header", recorded.getHeader("User-Agent"));
}

// Case 2: Caller sets User-Agent via an interceptor — value must be preserved.
@Test
public void callerOverride_userAgentPreserved() throws Exception {
server.enqueue(new MockResponse().setResponseCode(202));

ApiClient client = new ApiClient("key", "secret");
// Add after construction: runs after UserAgentInterceptor in the chain,
// so its User-Agent value reaches the network.
client.getOkBuilder().addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request modified = chain.request().newBuilder()
.header("User-Agent", "my-app/1.0")
.build();
return chain.proceed(modified);
}
});

EventsApi api = buildApi(client);
Call<Void> call = api.uploadEvents(new Batch().environment(Batch.Environment.DEVELOPMENT));
call.execute();

RecordedRequest recorded = server.takeRequest();
assertEquals("my-app/1.0", recorded.getHeader("User-Agent"));
}

// Case 3: configureFromOkclient path — no User-Agent should appear on the wire.
@Test
public void configureFromOkclient_noUserAgentHeader() throws Exception {
server.enqueue(new MockResponse().setResponseCode(202));

ApiClient client = new ApiClient("key", "secret");
OkHttpClient customClient = new OkHttpClient.Builder().build();
client.configureFromOkclient(customClient);

EventsApi api = buildApi(client);
Call<Void> call = api.uploadEvents(new Batch().environment(Batch.Environment.DEVELOPMENT));
call.execute();

RecordedRequest recorded = server.takeRequest();
assertNull("Expected no User-Agent header via configureFromOkclient", recorded.getHeader("User-Agent"));
}
}
Loading