From a7dc100a2431fcb85c92071feab592be7b1e8d99 Mon Sep 17 00:00:00 2001 From: abelonogov Date: Mon, 19 Oct 2026 17:37:04 -0700 Subject: [PATCH 1/3] feat: Compress event payloads with gzip by default Adds EventProcessorBuilder.enableCompression(boolean), defaulting to true, and passes it to the event sender in place of the hardcoded false, so analytics and diagnostic event payloads are sent gzip-encoded. Applications sending events through a relay proxy that does not accept gzip-encoded payloads can set it to false. Matches LDConfig.enableCompression in the iOS SDK. The contract test service now declares event-gzip and optional-event-gzip and applies enableGzip from the harness. Co-authored-by: Cursor --- .../launchdarkly/sdktest/Representations.java | 1 + .../launchdarkly/sdktest/SdkClientEntity.java | 3 + .../com/launchdarkly/sdktest/TestService.java | 2 + .../sdk/android/ComponentsImpl.java | 2 +- .../integrations/EventProcessorBuilder.java | 19 ++++++ .../sdk/android/EventCompressionTest.java | 65 +++++++++++++++++++ 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java index 4b7aa650a..7f1ddbc50 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java @@ -59,6 +59,7 @@ public static class SdkConfigEventParams { boolean enableDiagnostics; String[] globalPrivateAttributes; Long flushIntervalMs; + Boolean enableGzip; } public static class SdkConfigTagParams { diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index f96277041..3e43f8774 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -324,6 +324,9 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, if (params.events.globalPrivateAttributes != null) { eventsBuilder.privateAttributes(params.events.globalPrivateAttributes); } + if (params.events.enableGzip != null) { + eventsBuilder.enableCompression(params.events.enableGzip); + } builder.events(eventsBuilder); } diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java index 50acf5e51..0b2a43581 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java @@ -40,6 +40,8 @@ public class TestService extends NanoHTTPD { "client-prereq-events", "client-prereq-cycle-detection", "polling-gzip", + "event-gzip", + "optional-event-gzip", "evaluation-hooks", "track-hooks", "client-per-context-summaries", diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java index 34b47a2fe..8b84bcca1 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java @@ -112,7 +112,7 @@ public EventProcessor build(ClientContext clientContext) { StandardEndpoints.ANALYTICS_EVENTS_REQUEST_PATH, StandardEndpoints.DIAGNOSTIC_EVENTS_REQUEST_PATH, 0L, // use default retry delay - false, // disable gzip compression for Android + enableCompression, clientContext.getBaseLogger()), 1, // eventSendingThreadPoolSize clientContext.getServiceEndpoints().getEventsBaseUri(), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java index 989222ae2..e4085f599 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java @@ -61,6 +61,11 @@ public abstract class EventProcessorBuilder implements ComponentConfigurer + * By default this is {@code true}, which reduces egress bandwidth cost. Set it to {@code false} when + * sending events through a version of the Relay Proxy that does not accept gzip-encoded event payloads. + * + * @param enableCompression true to send event payloads gzip-encoded + * @return the builder + */ + public EventProcessorBuilder enableCompression(boolean enableCompression) { + this.enableCompression = enableCompression; + return this; + } + /** * Sets the interval between flushes of the event buffer. *

diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java new file mode 100644 index 000000000..d2863bb09 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java @@ -0,0 +1,65 @@ +package com.launchdarkly.sdk.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.subsystems.ClientContext; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; +import com.launchdarkly.testhelpers.httptest.Handlers; +import com.launchdarkly.testhelpers.httptest.HttpServer; +import com.launchdarkly.testhelpers.httptest.RequestInfo; + +import org.junit.Rule; +import org.junit.Test; + +public class EventCompressionTest { + private static final LDContext CONTEXT = LDContext.create("context-key"); + private static final String MOBILE_KEY = "test-mobile-key"; + private static final String EVENT_KEY = "compression-test-event"; + + @Rule + public LogCaptureRule logging = new LogCaptureRule(); + + @Test + public void eventPayloadsAreGzipEncodedByDefault() throws Exception { + RequestInfo request = sendOneEvent(Components.sendEvents()); + + assertEquals("gzip", request.getHeader("Content-Encoding")); + assertFalse("the payload should not be sent as plain JSON", request.getBody().contains(EVENT_KEY)); + } + + @Test + public void eventPayloadsAreSentUncompressedWhenCompressionIsDisabled() throws Exception { + RequestInfo request = sendOneEvent(Components.sendEvents().enableCompression(false)); + + assertNull(request.getHeader("Content-Encoding")); + assertTrue("the payload should be sent as plain JSON", request.getBody().contains(EVENT_KEY)); + } + + private RequestInfo sendOneEvent(EventProcessorBuilder eventsBuilder) throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(202))) { + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled) + .mobileKey(MOBILE_KEY) + .diagnosticOptOut(true) + .serviceEndpoints(Components.serviceEndpoints().events(server.getUri())) + .build(); + ClientContext clientContext = ClientContextImpl.fromConfig(config, MOBILE_KEY, "", + null, null, CONTEXT, logging.logger, new MockPlatformState(), + new EnvironmentReporterBuilder().build(), new SimpleTestTaskExecutor()); + + try (EventProcessor eventProcessor = eventsBuilder.build(clientContext)) { + eventProcessor.setOffline(false); + eventProcessor.recordCustomEvent(CONTEXT, EVENT_KEY, LDValue.ofNull(), null); + eventProcessor.flush(); + return server.getRecorder().requireRequest(); + } + } + } +} From 1c73c02ceb65fd8aa79e84f37f00f877e9f2ffeb Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Mon, 19 Oct 2026 18:23:32 -0700 Subject: [PATCH 2/3] fix gzip test --- .../com/launchdarkly/sdk/android/LDClientEventTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java index 544c416bd..25656d4f3 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java @@ -31,6 +31,8 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import okio.GzipSource; +import okio.Okio; public class LDClientEventTest { // For better test isolation, any tests here that need to inject flag data will use a simple @@ -520,12 +522,14 @@ public void testEventDoesNotContainAutoEnvAttributesWhenDisabled() throws Except } } - private LDValue[] getEventsFromLastRequest(MockWebServer server, int expectedCount) throws InterruptedException { + private LDValue[] getEventsFromLastRequest(MockWebServer server, int expectedCount) throws IOException, InterruptedException { RecordedRequest r = server.takeRequest(); assertEquals("POST", r.getMethod()); assertEquals("/mobile/events/bulk", r.getPath()); assertEquals(mobileKey, r.getHeader("Authorization")); - String body = r.getBody().readUtf8(); + String body = "gzip".equals(r.getHeader("Content-Encoding")) + ? Okio.buffer(new GzipSource(r.getBody())).readUtf8() + : r.getBody().readUtf8(); System.out.println(body); LDValue[] events = GsonHelpers.gsonInstance().fromJson(body, LDValue[].class); if (events.length != expectedCount) { From f34e0235a326e977867f5071484257f1a7c02dff Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Tue, 20 Oct 2026 08:25:06 -0700 Subject: [PATCH 3/3] Keep event compression off by default Older Relay Proxy versions do not accept gzip-encoded event payloads, so enableCompression is opt-in, matching iOS. Co-authored-by: Cursor --- .../integrations/EventProcessorBuilder.java | 7 ++++--- .../sdk/android/EventCompressionTest.java | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java index e4085f599..e16645df4 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java @@ -64,7 +64,7 @@ public abstract class EventProcessorBuilder implements ComponentConfigurer - * By default this is {@code true}, which reduces egress bandwidth cost. Set it to {@code false} when - * sending events through a version of the Relay Proxy that does not accept gzip-encoded event payloads. + * Compression reduces egress bandwidth cost. By default this is {@code false}, because older versions + * of the Relay Proxy do not accept gzip-encoded event payloads; applications not using the Relay Proxy + * are strongly encouraged to set it to {@code true}. * * @param enableCompression true to send event payloads gzip-encoded * @return the builder diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java index d2863bb09..87a70f444 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventCompressionTest.java @@ -28,19 +28,19 @@ public class EventCompressionTest { public LogCaptureRule logging = new LogCaptureRule(); @Test - public void eventPayloadsAreGzipEncodedByDefault() throws Exception { + public void eventPayloadsAreSentUncompressedByDefault() throws Exception { RequestInfo request = sendOneEvent(Components.sendEvents()); - assertEquals("gzip", request.getHeader("Content-Encoding")); - assertFalse("the payload should not be sent as plain JSON", request.getBody().contains(EVENT_KEY)); + assertNull(request.getHeader("Content-Encoding")); + assertTrue("the payload should be sent as plain JSON", request.getBody().contains(EVENT_KEY)); } @Test - public void eventPayloadsAreSentUncompressedWhenCompressionIsDisabled() throws Exception { - RequestInfo request = sendOneEvent(Components.sendEvents().enableCompression(false)); + public void eventPayloadsAreGzipEncodedWhenCompressionIsEnabled() throws Exception { + RequestInfo request = sendOneEvent(Components.sendEvents().enableCompression(true)); - assertNull(request.getHeader("Content-Encoding")); - assertTrue("the payload should be sent as plain JSON", request.getBody().contains(EVENT_KEY)); + assertEquals("gzip", request.getHeader("Content-Encoding")); + assertFalse("the payload should not be sent as plain JSON", request.getBody().contains(EVENT_KEY)); } private RequestInfo sendOneEvent(EventProcessorBuilder eventsBuilder) throws Exception {