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 4b7aa650..7f1ddbc5 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 f9627704..3e43f877 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 50acf5e5..0b2a4358 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/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java index 544c416b..25656d4f 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) { 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 34b47a2f..8b84bcca 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 989222ae..e16645df 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 + * 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 + */ + 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 00000000..87a70f44 --- /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 eventPayloadsAreSentUncompressedByDefault() throws Exception { + RequestInfo request = sendOneEvent(Components.sendEvents()); + + assertNull(request.getHeader("Content-Encoding")); + assertTrue("the payload should be sent as plain JSON", request.getBody().contains(EVENT_KEY)); + } + + @Test + public void eventPayloadsAreGzipEncodedWhenCompressionIsEnabled() throws Exception { + RequestInfo request = sendOneEvent(Components.sendEvents().enableCompression(true)); + + 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 { + 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(); + } + } + } +}