diff --git a/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventSender.java b/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventSender.java index 7b6d271..82ffbfb 100644 --- a/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventSender.java +++ b/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventSender.java @@ -34,7 +34,7 @@ public final class DefaultEventSender implements EventSender { * Default value for {@code retryDelayMillis} parameter. */ public static final long DEFAULT_RETRY_DELAY_MILLIS = 1000; - + /** * Default value for {@code analyticsRequestPath} parameter, for the server-side SDK. * The Android SDK should modify this value. @@ -46,7 +46,7 @@ public final class DefaultEventSender implements EventSender { * The Android SDK should modify this value. */ public static final String DEFAULT_DIAGNOSTIC_REQUEST_PATH = "/diagnostic"; - + private static final String EVENT_SCHEMA_HEADER = "X-LaunchDarkly-Event-Schema"; private static final String EVENT_SCHEMA_VERSION = "4"; private static final String EVENT_PAYLOAD_ID_HEADER = "X-LaunchDarkly-Payload-ID"; @@ -65,7 +65,7 @@ public final class DefaultEventSender implements EventSender { /** * Creates an instance. - * + * * @param httpProperties the HTTP configuration * @param analyticsRequestPath the request path for posting analytics events * @param diagnosticRequestPath the request path for posting diagnostic events @@ -91,20 +91,20 @@ public DefaultEventSender( this.baseHeaders = httpProperties.toHeadersBuilder() .add("Content-Type", "application/json") .build(); - + this.analyticsRequestPath = analyticsRequestPath == null ? DEFAULT_ANALYTICS_REQUEST_PATH : analyticsRequestPath; this.diagnosticRequestPath = diagnosticRequestPath == null ? DEFAULT_DIAGNOSTIC_REQUEST_PATH : diagnosticRequestPath; - + this.retryDelayMillis = retryDelayMillis <= 0 ? DEFAULT_RETRY_DELAY_MILLIS : retryDelayMillis; } - + @Override public void close() throws IOException { if (shouldCloseHttpClient) { HttpProperties.shutdownHttpClient(httpClient); } } - + @Override public Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri) { return sendEventData(false, data, eventCount, eventsBaseUri); @@ -114,20 +114,20 @@ public Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri public Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri) { return sendEventData(true, data, 1, eventsBaseUri); } - + private Result sendEventData(boolean isDiagnostic, byte[] data, int eventCount, URI eventsBaseUri) { if (data == null || data.length == 0) { // DefaultEventProcessor won't normally pass us an empty payload, but if it does, don't bother sending return new Result(true, false, null); } - + Headers.Builder headersBuilder = baseHeaders.newBuilder(); String path; String description; - + if (isDiagnostic) { path = diagnosticRequestPath; - description = "diagnostic event"; + description = "diagnostic event"; } else { path = analyticsRequestPath; String eventPayloadId = UUID.randomUUID().toString(); @@ -135,12 +135,12 @@ private Result sendEventData(boolean isDiagnostic, byte[] data, int eventCount, headersBuilder.add(EVENT_SCHEMA_HEADER, EVENT_SCHEMA_VERSION); description = String.format("%d event(s)", eventCount); } - + URI uri = HttpHelpers.concatenateUriPath(eventsBaseUri, path); Headers headers = headersBuilder.build(); RequestBody body = RequestBody.create(data, JSON_CONTENT_TYPE); boolean mustShutDown = false; - + logger.debug("Posting {} to {} with payload: {}", description, uri, LogValues.defer(new LazilyPrintedUtf8Data(data))); @@ -162,15 +162,15 @@ private Result sendEventData(boolean isDiagnostic, byte[] data, int eventCount, long startTime = System.currentTimeMillis(); String nextActionMessage = attempt == 0 ? "will retry" : "some events were dropped"; String errorContext = "posting " + description; - + try (Response response = httpClient.newCall(request).execute()) { long endTime = System.currentTimeMillis(); logger.debug("{} delivery took {} ms, response status {}", description, endTime - startTime, response.code()); - + if (response.isSuccessful()) { return new Result(true, false, parseResponseDate(response)); } - + String errorDesc = httpErrorDescription(response.code()); boolean recoverable = checkIfErrorIsRecoverableAndLog( logger, @@ -187,10 +187,10 @@ private Result sendEventData(boolean isDiagnostic, byte[] data, int eventCount, checkIfErrorIsRecoverableAndLog(logger, e.toString(), errorContext, 0, nextActionMessage); } } - + return new Result(false, mustShutDown, null); } - + private final Date parseResponseDate(Response response) { String dateStr = response.header("Date"); if (dateStr != null) { @@ -205,10 +205,10 @@ private final Date parseResponseDate(Response response) { } return null; } - + private final class LazilyPrintedUtf8Data implements LogValues.StringProvider { private final byte[] data; - + LazilyPrintedUtf8Data(byte[] data) { this.data = data; } diff --git a/src/main/java/com/launchdarkly/sdk/internal/events/EventContextFormatter.java b/src/main/java/com/launchdarkly/sdk/internal/events/EventContextFormatter.java index 1dcfcad..878ed79 100644 --- a/src/main/java/com/launchdarkly/sdk/internal/events/EventContextFormatter.java +++ b/src/main/java/com/launchdarkly/sdk/internal/events/EventContextFormatter.java @@ -30,25 +30,25 @@ class EventContextFormatter { this.allAttributesPrivate = allAttributesPrivate; this.globalPrivateAttributes = globalPrivateAttributes == null ? new AttributeRef[0] : globalPrivateAttributes; } - - public void write(LDContext c, JsonWriter w) throws IOException { + + public void write(LDContext c, JsonWriter w, boolean redactAnonymous) throws IOException { if (c.isMultiple()) { w.beginObject(); w.name("kind").value("multi"); for (int i = 0; i < c.getIndividualContextCount(); i++) { LDContext c1 = c.getIndividualContext(i); w.name(c1.getKind().toString()); - writeSingleKind(c1, w, false); + writeSingleKind(c1, w, false, redactAnonymous); } w.endObject(); } else { - writeSingleKind(c, w, true); + writeSingleKind(c, w, true, redactAnonymous); } } - - private void writeSingleKind(LDContext c, JsonWriter w, boolean includeKind) throws IOException { + + private void writeSingleKind(LDContext c, JsonWriter w, boolean includeKind, boolean redactAnonymous) throws IOException { w.beginObject(); - + // kind, key, and anonymous are never redacted if (includeKind) { w.name("kind").value(c.getKind().toString()); @@ -57,21 +57,20 @@ private void writeSingleKind(LDContext c, JsonWriter w, boolean includeKind) thr if (c.isAnonymous()) { w.name("anonymous").value(true); } - + List redacted = null; - if (c.getName() != null) { - if (isAttributeEntirelyPrivate(c, "name")) { + if (isAttributeEntirelyPrivate(c, "name", redactAnonymous)) { redacted = addOrCreate(redacted, "name"); } else { w.name("name").value(c.getName()); } } - + for (String attrName: c.getCustomAttributeNames()) { - redacted = writeOrRedactAttribute(w, c, attrName, c.getValue(attrName), redacted); + redacted = writeOrRedactAttribute(w, c, attrName, c.getValue(attrName), redacted, redactAnonymous); } - + boolean haveRedacted = redacted != null && !redacted.isEmpty(); if (haveRedacted) { w.name("_meta").beginObject(); @@ -82,31 +81,36 @@ private void writeSingleKind(LDContext c, JsonWriter w, boolean includeKind) thr w.endArray(); w.endObject(); } - + w.endObject(); } - - private boolean isAttributeEntirelyPrivate(LDContext c, String attrName) { + + private boolean isAttributeEntirelyPrivate(LDContext c, String attrName, boolean redactAnonymous) { if (allAttributesPrivate) { return true; + } else if (redactAnonymous && c.isAnonymous()) { + return true; } AttributeRef privateRef = findPrivateRef(c, 1, attrName, null); return privateRef != null && privateRef.getDepth() == 1; } - + private List writeOrRedactAttribute( JsonWriter w, LDContext c, String attrName, LDValue value, - List redacted + List redacted, + boolean redactAnonymous ) throws IOException { if (allAttributesPrivate) { return addOrCreate(redacted, attrName); + } else if (redactAnonymous && c.isAnonymous()) { + return addOrCreate(redacted, attrName); } return writeRedactedValue(w, c, 0, attrName, value, null, redacted); } - + // This method implements the context-aware attribute redaction logic, in which an attribute // can be 1. written as-is, 2. fully redacted, or 3. (for a JSON object) partially redacted. // It returns the updated redacted attribute list. diff --git a/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java b/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java index 538e8b6..3f0c25d 100644 --- a/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java +++ b/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java @@ -22,19 +22,19 @@ *

* Test coverage for this logic is in EventOutputTest and DefaultEventProcessorOutputTest. The * handling of context data and private attribute redaction is implemented in EventContextFormatter - * and tested in more detail in EventContextFormatterTest. + * and tested in more detail in EventContextFormatterTest. */ final class EventOutputFormatter { private final EventContextFormatter contextFormatter; - + EventOutputFormatter(EventsConfiguration config) { this.contextFormatter = new EventContextFormatter( config.allAttributesPrivate, config.privateAttributes.toArray(new AttributeRef[config.privateAttributes.size()])); } - + int writeOutputEvents(Event[] events, EventSummarizer.EventSummary summary, Writer writer) throws IOException { - int count = 0; + int count = 0; JsonWriter jsonWriter = new JsonWriter(writer); jsonWriter.beginArray(); for (Event event: events) { @@ -50,7 +50,7 @@ int writeOutputEvents(Event[] events, EventSummarizer.EventSummary summary, Writ jsonWriter.flush(); return count; } - + private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException { if (event.getContext() == null || !event.getContext().isValid()) { // The SDK should never send us an event without a valid context, but if we somehow get one, @@ -62,11 +62,7 @@ private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException jw.beginObject(); writeKindAndCreationDate(jw, fe.isDebug() ? "debug" : "feature", event.getCreationDate()); jw.name("key").value(fe.getKey()); - if (fe.isDebug()) { - writeContext(fe.getContext(), jw); - } else { - writeContextKeys(fe.getContext(), jw); - } + writeContext(fe.getContext(), jw, !fe.isDebug()); if (fe.getVersion() >= 0) { jw.name("version"); jw.value(fe.getVersion()); @@ -86,7 +82,7 @@ private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException } else if (event instanceof Event.Identify) { jw.beginObject(); writeKindAndCreationDate(jw, "identify", event.getCreationDate()); - writeContext(event.getContext(), jw); + writeContext(event.getContext(), jw, false); jw.endObject(); } else if (event instanceof Event.Custom) { Event.Custom ce = (Event.Custom)event; @@ -103,7 +99,7 @@ private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException } else if (event instanceof Event.Index) { jw.beginObject(); writeKindAndCreationDate(jw, "index", event.getCreationDate()); - writeContext(event.getContext(), jw); + writeContext(event.getContext(), jw, false); jw.endObject(); } else if (event instanceof Event.MigrationOp) { jw.beginObject(); @@ -229,44 +225,44 @@ private void writeMigrationEvaluation(JsonWriter jw, Event.MigrationOp me) throw private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter jw) throws IOException { jw.beginObject(); - + jw.name("kind"); jw.value("summary"); - + jw.name("startDate"); jw.value(summary.startDate); jw.name("endDate"); jw.value(summary.endDate); - + jw.name("features"); jw.beginObject(); - + for (Map.Entry flag: summary.counters.entrySet()) { String flagKey = flag.getKey(); FlagInfo flagInfo = flag.getValue(); - + jw.name(flagKey); jw.beginObject(); - + writeLDValue("default", flagInfo.defaultVal, jw); jw.name("contextKinds").beginArray(); for (String kind: flagInfo.contextKinds) { jw.value(kind); } jw.endArray(); - + jw.name("counters"); jw.beginArray(); - + for (int i = 0; i < flagInfo.versionsAndVariations.size(); i++) { int version = flagInfo.versionsAndVariations.keyAt(i); SimpleIntKeyedMap variations = flagInfo.versionsAndVariations.valueAt(i); for (int j = 0; j < variations.size(); j++) { int variation = variations.keyAt(j); CounterValue counter = variations.valueAt(j); - + jw.beginObject(); - + if (variation >= 0) { jw.name("variation").value(variation); } @@ -277,7 +273,7 @@ private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter } writeLDValue("value", counter.flagValue, jw); jw.name("count").value(counter.count); - + jw.endObject(); } } @@ -285,21 +281,21 @@ private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter jw.endArray(); // end of "counters" array jw.endObject(); // end of this flag } - + jw.endObject(); // end of "features" jw.endObject(); // end of summary event object } - + private void writeKindAndCreationDate(JsonWriter jw, String kind, long creationDate) throws IOException { jw.name("kind").value(kind); jw.name("creationDate").value(creationDate); } - - private void writeContext(LDContext context, JsonWriter jw) throws IOException { + + private void writeContext(LDContext context, JsonWriter jw, boolean redactAnonymous) throws IOException { jw.name("context"); - contextFormatter.write(context, jw); + contextFormatter.write(context, jw, redactAnonymous); } - + private void writeContextKeys(LDContext context, JsonWriter jw) throws IOException { jw.name("contextKeys").beginObject(); for (int i = 0; i < context.getIndividualContextCount(); i++) { @@ -310,7 +306,7 @@ private void writeContextKeys(LDContext context, JsonWriter jw) throws IOExcepti } jw.endObject(); } - + private void writeLDValue(String key, LDValue value, JsonWriter jw) throws IOException { if (value == null || value.isNull()) { return; @@ -318,7 +314,7 @@ private void writeLDValue(String key, LDValue value, JsonWriter jw) throws IOExc jw.name(key); gsonInstance().toJson(value, LDValue.class, jw); // LDValue defines its own custom serializer } - + private void writeEvaluationReason(EvaluationReason er, JsonWriter jw) throws IOException { if (er == null) { return; diff --git a/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java b/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java index d59bd98..3ef138e 100644 --- a/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java +++ b/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java @@ -62,7 +62,7 @@ public abstract class BaseEventTest extends BaseTest { public static void assertJsonEquals(LDValue expected, LDValue actual) { JsonAssertions.assertJsonEquals(expected.toJsonString(), actual.toJsonString()); } - + public static EventsConfigurationBuilder baseConfig(EventSender es) { return new EventsConfigurationBuilder().eventSender(es); } @@ -94,7 +94,7 @@ public static EventsConfiguration makeEventsConfig(boolean allAttributesPrivate, .privateAttributes(privateAttributes == null ? null : new HashSet<>(privateAttributes)) .build(); } - + public static EvaluationDetail simpleEvaluation(int variation, LDValue value) { return EvaluationDetail.fromValue(value, variation, EvaluationReason.off()); } @@ -104,7 +104,7 @@ static final class CapturedPayload { final String data; final int eventCount; final URI eventsBaseUri; - + CapturedPayload(boolean diagnostic, String data, int eventCount, URI eventsBaseUri) { this.diagnostic = diagnostic; this.data = data; @@ -121,9 +121,9 @@ public final class MockEventSender implements EventSender { volatile IOException fakeErrorOnClose = null; volatile CountDownLatch receivedCounter = null; volatile Object waitSignal = null; - + final BlockingQueue receivedParams = new LinkedBlockingQueue<>(); - + @Override public Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri) { testLogger.debug("[MockEventSender] received {} events: {}", eventCount, new String(data)); @@ -135,7 +135,7 @@ public Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri) { testLogger.debug("[MockEventSender] received diagnostic event: {}", new String(data)); return receive(true, data, 1, eventsBaseUri); } - + @Override public void close() throws IOException { closed = true; @@ -147,7 +147,7 @@ public void close() throws IOException { private Result receive(boolean diagnostic, byte[] data, int eventCount, URI eventsBaseUri) { receivedParams.add(new CapturedPayload(diagnostic, new String(data, Charset.forName("UTF-8")), eventCount, eventsBaseUri)); if (waitSignal != null) { - // this is used in DefaultEventProcessorTest.eventsAreKeptInBufferIfAllFlushWorkersAreBusy + // this is used in DefaultEventProcessorTest.eventsAreKeptInBufferIfAllFlushWorkersAreBusy synchronized (waitSignal) { if (receivedCounter != null) { receivedCounter.countDown(); @@ -162,7 +162,7 @@ private Result receive(boolean diagnostic, byte[] data, int eventCount, URI even } return result; } - + CapturedPayload awaitRequest() { return awaitValue(receivedParams, 5, TimeUnit.SECONDS); } @@ -172,17 +172,17 @@ CapturedPayload awaitAnalytics() { assertFalse("expected analytics event but got diagnostic event instead", p.diagnostic); return p; } - + CapturedPayload awaitDiagnostic() { CapturedPayload p = awaitValue(receivedParams, 5, TimeUnit.SECONDS); assertTrue("expected a diagnostic event but got analytics events instead", p.diagnostic); return p; } - + void expectNoRequests(long timeoutMillis) { assertNoMoreValues(receivedParams, timeoutMillis, TimeUnit.MILLISECONDS); } - + List getEventsFromLastRequest() { CapturedPayload p = awaitRequest(); LDValue a = LDValue.parse(p.data); @@ -224,8 +224,8 @@ public static Matcher isIndexEvent(Event sourceEvent, LDValue con ); } - public static Matcher isFeatureEvent(Event.FeatureRequest sourceEvent) { - return isFeatureOrDebugEvent(sourceEvent, null, false); + public static Matcher isFeatureEvent(Event.FeatureRequest sourceEvent, LDValue inlineContext) { + return isFeatureOrDebugEvent(sourceEvent, inlineContext, false); } public static Matcher isDebugEvent(Event.FeatureRequest sourceEvent, LDValue inlineContext) { @@ -242,7 +242,7 @@ private static Matcher isFeatureOrDebugEvent(Event.FeatureRequest jsonProperty("version", sourceEvent.getVersion()), jsonProperty("variation", sourceEvent.getVariation()), jsonProperty("value", jsonFromValue(sourceEvent.getValue())), - inlineContext == null ? hasContextKeys(sourceEvent) : hasInlineContext(inlineContext), + hasInlineContext(inlineContext), jsonProperty("reason", sourceEvent.getReason() == null ? jsonUndefined() : jsonEqualsValue(sourceEvent.getReason())), jsonProperty("prereqOf", sourceEvent.getPrereqOf() == null ? jsonUndefined() : jsonEqualsValue(sourceEvent.getPrereqOf())) ); @@ -256,7 +256,7 @@ public static Matcher isCustomEvent(Event.Custom sourceEvent) { jsonProperty("key", sourceEvent.getKey()), hasContextKeys(sourceEvent), jsonProperty("data", hasData ? jsonEqualsValue(sourceEvent.getData()) : jsonUndefined()), - jsonProperty("metricValue", sourceEvent.getMetricValue() == null ? jsonUndefined() : jsonEqualsValue(sourceEvent.getMetricValue())) + jsonProperty("metricValue", sourceEvent.getMetricValue() == null ? jsonUndefined() : jsonEqualsValue(sourceEvent.getMetricValue())) ); } @@ -269,14 +269,14 @@ public static Matcher hasContextKeys(Event sourceEvent) { } return jsonProperty("contextKeys", jsonEqualsValue(b.build())); } - + public static Matcher hasInlineContext(LDValue inlineContext) { return allOf( jsonProperty("context", jsonEqualsValue(inlineContext)), jsonProperty("contextKeys", jsonUndefined()) ); } - + public static Matcher isSummaryEvent() { return jsonProperty("kind", "summary"); } @@ -288,7 +288,7 @@ public static Matcher isSummaryEvent(long startDate, long endDate jsonProperty("endDate", (double)endDate) ); } - + public static Matcher hasSummaryFlag(String key, LDValue defaultVal, Matcher> counters) { return jsonProperty("features", jsonProperty(key, allOf( @@ -296,7 +296,7 @@ public static Matcher hasSummaryFlag(String key, LDValue defaultV jsonProperty("counters", isJsonArray(counters)) ))); } - + public static Matcher isSummaryEventCounter(int flagVersion, Integer variation, LDValue value, int count) { return allOf( jsonProperty("variation", variation), @@ -313,11 +313,11 @@ public static FeatureRequestEventBuilder featureEvent(LDContext context, String public static CustomEventBuilder customEvent(LDContext context, String flagKey) { return new CustomEventBuilder(context, flagKey); } - + public static Event.Identify identifyEvent(LDContext context) { return new Event.Identify(FAKE_TIME, context); } - + /** * This builder is similar to the public SDK configuration builder for events, except it is building * the internal config object for the lower-level event processing code. This allows us to test that @@ -325,10 +325,10 @@ public static Event.Identify identifyEvent(LDContext context) { * the same as the defaults in the SDK; they are chosen to make it unlikely for tests to be affected * by any behavior we're not specifically trying to test-- for instance, a long flush interval means * that flushes normally won't happen, and any test where we want flushes to happen will not rely on - * the defaults. + * the defaults. *

* This is defined only in test code, instead of as an inner class of EventsConfiguration, because - * in non-test code there's only one place where we ever construct EventsConfiguration. + * in non-test code there's only one place where we ever construct EventsConfiguration. */ public static class EventsConfigurationBuilder { private boolean allAttributesPrivate = false; @@ -360,12 +360,12 @@ public EventsConfiguration build() { privateAttributes ); } - + public EventsConfigurationBuilder allAttributesPrivate(boolean allAttributesPrivate) { this.allAttributesPrivate = allAttributesPrivate; return this; } - + public EventsConfigurationBuilder capacity(int capacity) { this.capacity = capacity; return this; @@ -375,32 +375,32 @@ public EventsConfigurationBuilder contextDeduplicator(EventContextDeduplicator c this.contextDeduplicator = contextDeduplicator; return this; } - + public EventsConfigurationBuilder diagnosticRecordingIntervalMillis(long diagnosticRecordingIntervalMillis) { this.diagnosticRecordingIntervalMillis = diagnosticRecordingIntervalMillis; return this; } - + public EventsConfigurationBuilder diagnosticStore(DiagnosticStore diagnosticStore) { this.diagnosticStore = diagnosticStore; return this; } - + public EventsConfigurationBuilder eventSender(EventSender eventSender) { this.eventSender = eventSender; return this; } - + public EventsConfigurationBuilder eventSendingThreadPoolSize(int eventSendingThreadPoolSize) { this.eventSendingThreadPoolSize = eventSendingThreadPoolSize; return this; } - + public EventsConfigurationBuilder eventsUri(URI eventsUri) { this.eventsUri = eventsUri; return this; } - + public EventsConfigurationBuilder flushIntervalMillis(long flushIntervalMillis) { this.flushIntervalMillis = flushIntervalMillis; return this; @@ -415,13 +415,13 @@ public EventsConfigurationBuilder initiallyOffline(boolean initiallyOffline) { this.initiallyOffline = initiallyOffline; return this; } - + public EventsConfigurationBuilder privateAttributes(Set privateAttributes) { this.privateAttributes = privateAttributes; return this; } } - + public static EventContextDeduplicator contextDeduplicatorThatAlwaysSaysKeysAreNew() { return new EventContextDeduplicator() { @Override @@ -438,11 +438,11 @@ public boolean processContext(LDContext context) { public void flush() {} }; } - + public static EventContextDeduplicator contextDeduplicatorThatSaysKeyIsNewOnFirstCallOnly() { return new EventContextDeduplicator() { private int calls = 0; - + @Override public Long getFlushInterval() { return null; @@ -456,9 +456,9 @@ public boolean processContext(LDContext context) { @Override public void flush() {} - }; + }; } - + public static final class FeatureRequestEventBuilder { private long timestamp = FAKE_TIME; private LDContext context; @@ -473,43 +473,43 @@ public static final class FeatureRequestEventBuilder { private Long debugEventsUntilDate = null; private long samplingRatio = 1; private boolean excludeFromSummaries = false; - + public FeatureRequestEventBuilder(LDContext context, String flagKey) { this.context = context; this.flagKey = flagKey; } - + public Event.FeatureRequest build() { return new Event.FeatureRequest(timestamp, flagKey, context, flagVersion, variation, value, defaultValue, reason, prereqOf, trackEvents, debugEventsUntilDate, false, samplingRatio, excludeFromSummaries); } - + public FeatureRequestEventBuilder flagVersion(int flagVersion) { this.flagVersion = flagVersion; return this; } - + public FeatureRequestEventBuilder variation(int variation) { this.variation = variation; return this; } - + public FeatureRequestEventBuilder value(LDValue value) { this.value = value; return this; } - + public FeatureRequestEventBuilder defaultValue(LDValue defaultValue) { this.defaultValue = defaultValue; return this; } - + public FeatureRequestEventBuilder reason(EvaluationReason reason) { this.reason = reason; return this; } - + public FeatureRequestEventBuilder prereqOf(String prereqOf) { this.prereqOf = prereqOf; return this; @@ -535,28 +535,28 @@ public FeatureRequestEventBuilder samplingRatio(long samplingRatio) { return this; } } - + public static final class CustomEventBuilder { private long timestamp = FAKE_TIME; private LDContext context; private String eventKey; private LDValue data = LDValue.ofNull(); private Double metricValue = null; - + public CustomEventBuilder(LDContext context, String eventKey) { this.context = context; this.eventKey = eventKey; } - + public Event.Custom build() { return new Event.Custom(timestamp, eventKey, context, data, metricValue); } - + public CustomEventBuilder data(LDValue data) { this.data = data; return this; } - + public CustomEventBuilder metricValue(Double metricValue) { this.metricValue = metricValue; return this; diff --git a/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java b/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java index 55a505b..684004a 100644 --- a/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java +++ b/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java @@ -22,13 +22,13 @@ @SuppressWarnings("javadoc") public class DefaultEventProcessorOutputTest extends BaseEventTest { private static final LDContext invalidContext = LDContext.create(null); - + // Note: context deduplication behavior has been abstracted out of DefaultEventProcessor, so that // by default it does not generate any index events. Test cases in this file that are not // specifically related to index events use this default behavior, and do not expect to see any. // When we are specifically testing this behavior, we substitute a mock EventContextDeduplicator // so we can verify how its outputs affect DefaultEventProcessor. - + @Test public void identifyEventIsQueued() throws Exception { MockEventSender es = new MockEventSender(); @@ -42,7 +42,7 @@ public void identifyEventIsQueued() throws Exception { isIdentifyEvent(e, userJson) )); } - + @Test public void userIsFilteredInIdentifyEvent() throws Exception { MockEventSender es = new MockEventSender(); @@ -84,14 +84,14 @@ public void individualFeatureEventIsQueuedWithIndexEvent() throws Exception { Event.FeatureRequest fe = featureEvent(user, FLAG_KEY).trackEvents(true).build(); EventContextDeduplicator contextDeduplicator = contextDeduplicatorThatAlwaysSaysKeysAreNew(); - + try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).contextDeduplicator(contextDeduplicator))) { ep.sendEvent(fe); } - + assertThat(es.getEventsFromLastRequest(), contains( isIndexEvent(fe, userJson), - isFeatureEvent(fe), + isFeatureEvent(fe, userJson), isSummaryEvent() )); } @@ -130,7 +130,7 @@ public void featureEventCanBeExcludedFromSummaries() throws Exception { List events = es.getEventsFromLastRequest(); assertThat(events, contains( isIndexEvent(fe, userJson), - isFeatureEvent(fe) + isFeatureEvent(fe, userJson) )); // No feature event. Assert.assertEquals(2, events.size()); @@ -147,7 +147,7 @@ public void userIsFilteredInIndexEvent() throws Exception { try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).allAttributesPrivate(true).contextDeduplicator(contextDeduplicator))) { ep.sendEvent(fe); } - + assertThat(es.getEventsFromLastRequest(), contains( isIndexEvent(fe, filteredUserJson), isSummaryEvent() @@ -164,9 +164,9 @@ public void featureEventCanBeForPrerequisite() throws Exception { try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { ep.sendEvent(fe); } - + assertThat(es.getEventsFromLastRequest(), contains( - isFeatureEvent(fe), + isFeatureEvent(fe, userJson), isSummaryEvent() )); } @@ -184,12 +184,12 @@ public void featureEventWithNullContextOrInvalidContextIsIgnored() throws Except ep.sendEvent(event1); ep.sendEvent(event2); } - + assertThat(es.getEventsFromLastRequest(), contains( isSummaryEvent() )); } - + @SuppressWarnings("unchecked") @Test public void featureEventCanContainReason() throws Exception { @@ -202,11 +202,11 @@ public void featureEventCanContainReason() throws Exception { } assertThat(es.getEventsFromLastRequest(), contains( - isFeatureEvent(fe), + isFeatureEvent(fe, userJson), isSummaryEvent() )); } - + @SuppressWarnings("unchecked") @Test public void eventKindIsDebugIfFlagIsTemporarilyInDebugMode() throws Exception { @@ -217,13 +217,13 @@ public void eventKindIsDebugIfFlagIsTemporarilyInDebugMode() throws Exception { try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { ep.sendEvent(fe); } - + assertThat(es.getEventsFromLastRequest(), contains( isDebugEvent(fe, userJson), isSummaryEvent() )); } - + @SuppressWarnings("unchecked") @Test public void eventCanBeBothTrackedAndDebugged() throws Exception { @@ -236,20 +236,20 @@ public void eventCanBeBothTrackedAndDebugged() throws Exception { } assertThat(es.getEventsFromLastRequest(), contains( - isFeatureEvent(fe), + isFeatureEvent(fe, userJson), isDebugEvent(fe, userJson), isSummaryEvent() )); } - + @Test public void debugModeExpiresBasedOnClientTimeIfClientTimeIsLaterThanServerTime() throws Exception { MockEventSender es = new MockEventSender(); - + // Pick a server time that is somewhat behind the client time long serverTime = System.currentTimeMillis() - 20000; es.result = new EventSender.Result(true, false, new Date(serverTime)); - + long debugUntil = serverTime + 1000; Event.FeatureRequest fe = featureEvent(user, FLAG_KEY).debugEventsUntilDate(debugUntil).build(); @@ -258,15 +258,15 @@ public void debugModeExpiresBasedOnClientTimeIfClientTimeIsLaterThanServerTime() ep.sendEvent(identifyEvent(LDContext.create("otherUser"))); ep.flushBlocking(); // wait till flush is done so we know we received the first response, with the date es.awaitRequest(); - + es.receivedParams.clear(); es.result = new EventSender.Result(true, false, null); - + // Now send an event with debug mode on, with a "debug until" time that is further in // the future than the server time, but in the past compared to the client. ep.sendEvent(fe); } - + // Should get a summary event only, not a full feature event assertThat(es.getEventsFromLastRequest(), contains( isSummaryEvent(fe.getCreationDate(), fe.getCreationDate()) @@ -276,11 +276,11 @@ public void debugModeExpiresBasedOnClientTimeIfClientTimeIsLaterThanServerTime() @Test public void debugModeExpiresBasedOnServerTimeIfServerTimeIsLaterThanClientTime() throws Exception { MockEventSender es = new MockEventSender(); - + // Pick a server time that is somewhat ahead of the client time long serverTime = System.currentTimeMillis() + 20000; es.result = new EventSender.Result(true, false, new Date(serverTime)); - + long debugUntil = serverTime - 1000; Event.FeatureRequest fe = featureEvent(user, FLAG_KEY).debugEventsUntilDate(debugUntil).build(); @@ -289,7 +289,7 @@ public void debugModeExpiresBasedOnServerTimeIfServerTimeIsLaterThanClientTime() ep.sendEvent(identifyEvent(LDContext.create("otherUser"))); ep.flushBlocking(); // wait till flush is done so we know we received the first response, with the date es.awaitRequest(); - + es.receivedParams.clear(); es.result = new EventSender.Result(true, false, null); @@ -297,13 +297,13 @@ public void debugModeExpiresBasedOnServerTimeIfServerTimeIsLaterThanClientTime() // the future than the client time, but in the past compared to the server. ep.sendEvent(fe); } - + // Should get a summary event only, not a full feature event assertThat(es.getEventsFromLastRequest(), contains( isSummaryEvent(fe.getCreationDate(), fe.getCreationDate()) )); } - + @SuppressWarnings("unchecked") @Test public void twoFeatureEventsForSameContextGenerateOnlyOneIndexEvent() throws Exception { @@ -311,7 +311,7 @@ public void twoFeatureEventsForSameContextGenerateOnlyOneIndexEvent() throws Exc // EventContextDeduplicator says about whether a context key is new or not. We will set up // an EventContextDeduplicator that reports "new" on the first call and "not new" on the 2nd. EventContextDeduplicator contextDeduplicator = contextDeduplicatorThatSaysKeyIsNewOnFirstCallOnly(); - + MockEventSender es = new MockEventSender(); Event.FeatureRequest fe1 = featureEvent(user, "flagkey1").trackEvents(true).build(); Event.FeatureRequest fe2 = featureEvent(user, "flagkey2").trackEvents(true).build(); @@ -320,11 +320,11 @@ public void twoFeatureEventsForSameContextGenerateOnlyOneIndexEvent() throws Exc ep.sendEvent(fe1); ep.sendEvent(fe2); } - + assertThat(es.getEventsFromLastRequest(), contains( isIndexEvent(fe1, userJson), - isFeatureEvent(fe1), - isFeatureEvent(fe2), + isFeatureEvent(fe1, userJson), + isFeatureEvent(fe2, userJson), isSummaryEvent(fe1.getCreationDate(), fe2.getCreationDate()) )); } @@ -338,17 +338,17 @@ public void identifyEventMakesIndexEventUnnecessary() throws Exception { try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { ep.sendEvent(ie); - ep.sendEvent(fe); + ep.sendEvent(fe); } - + assertThat(es.getEventsFromLastRequest(), contains( isIdentifyEvent(ie, userJson), - isFeatureEvent(fe), + isFeatureEvent(fe, userJson), isSummaryEvent() )); } - + @SuppressWarnings("unchecked") @Test public void nonTrackedEventsAreSummarized() throws Exception { @@ -372,7 +372,7 @@ public void nonTrackedEventsAreSummarized() throws Exception { ep.sendEvent(fe1c); ep.sendEvent(fe2); } - + assertThat(es.getEventsFromLastRequest(), contains( allOf( isSummaryEvent(fe1a.getCreationDate(), fe2.getCreationDate()), @@ -386,7 +386,7 @@ public void nonTrackedEventsAreSummarized() throws Exception { ) )); } - + @Test public void customEventIsQueuedWithUser() throws Exception { MockEventSender es = new MockEventSender(); @@ -397,12 +397,12 @@ public void customEventIsQueuedWithUser() throws Exception { try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { ep.sendEvent(ce); } - + assertThat(es.getEventsFromLastRequest(), contains( isCustomEvent(ce) )); } - + @Test public void customEventWithNullContextOrInvalidContextDoesNotCauseError() throws Exception { // This should never happen because LDClient rejects such a user, but just in case, @@ -411,13 +411,13 @@ public void customEventWithNullContextOrInvalidContextDoesNotCauseError() throws Event.Custom event1 = customEvent(invalidContext, "eventkey").build(); Event.Custom event2 = customEvent(null, "eventkey").build(); Event.Custom event3 = customEvent(user, "eventkey").build(); - + try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { ep.sendEvent(event1); ep.sendEvent(event2); ep.sendEvent(event3); } - + assertThat(es.getEventsFromLastRequest(), contains( isCustomEvent(event3) )); diff --git a/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventSenderTest.java b/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventSenderTest.java index 180536f..3084475 100644 --- a/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventSenderTest.java +++ b/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventSenderTest.java @@ -45,7 +45,7 @@ protected boolean enableTestInAndroid() { // package that performs end-to-end HTTP. return false; } - + private EventSender makeEventSender() { return makeEventSender(HttpProperties.defaults()); } @@ -53,18 +53,18 @@ private EventSender makeEventSender() { private EventSender makeEventSender(HttpProperties httpProperties) { return new DefaultEventSender(httpProperties, null, null, BRIEF_RETRY_DELAY_MILLIS, testLogger); } - + @Test public void analyticsDataIsDelivered() throws Exception { try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { try (EventSender es = makeEventSender()) { EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); - + assertTrue(result.isSuccess()); assertFalse(result.isMustShutDown()); } - - RequestInfo req = server.getRecorder().requireRequest(); + + RequestInfo req = server.getRecorder().requireRequest(); assertEquals(DefaultEventSender.DEFAULT_ANALYTICS_REQUEST_PATH, req.getPath()); assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); assertEquals(FAKE_DATA, req.getBody()); @@ -76,12 +76,12 @@ public void diagnosticDataIsDelivered() throws Exception { try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { try (EventSender es = makeEventSender()) { EventSender.Result result = es.sendDiagnosticEvent(FAKE_DATA_BYTES, server.getUri()); - + assertTrue(result.isSuccess()); assertFalse(result.isMustShutDown()); } - - RequestInfo req = server.getRecorder().requireRequest(); + + RequestInfo req = server.getRecorder().requireRequest(); assertEquals(DefaultEventSender.DEFAULT_DIAGNOSTIC_REQUEST_PATH, req.getPath()); assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); assertEquals(FAKE_DATA, req.getBody()); @@ -98,10 +98,10 @@ public void customRequestPaths() throws Exception { result = es.sendDiagnosticEvent(FAKE_DATA_BYTES, server.getUri()); assertTrue(result.isSuccess()); } - - RequestInfo req1 = server.getRecorder().requireRequest(); + + RequestInfo req1 = server.getRecorder().requireRequest(); assertEquals("/custom/path/a", req1.getPath()); - RequestInfo req2 = server.getRecorder().requireRequest(); + RequestInfo req2 = server.getRecorder().requireRequest(); assertEquals("/custom/path/d", req2.getPath()); } } @@ -112,12 +112,12 @@ public void headersAreSentForAnalytics() throws Exception { headers.put("name1", "value1"); headers.put("name2", "value2"); HttpProperties httpProperties = new HttpProperties(0, headers, null, null, null, null, 0, null, null); - + try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { try (EventSender es = makeEventSender(httpProperties)) { es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); } - + RequestInfo req = server.getRecorder().requireRequest(); for (Map.Entry kv: headers.entrySet()) { assertThat(req.getHeader(kv.getKey()), equalTo(kv.getValue())); @@ -131,13 +131,13 @@ public void headersAreSentForDiagnostics() throws Exception { headers.put("name1", "value1"); headers.put("name2", "value2"); HttpProperties httpProperties = new HttpProperties(0, headers, null, null, null, null, 0, null, null); - + try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { try (EventSender es = makeEventSender(httpProperties)) { es.sendDiagnosticEvent(FAKE_DATA_BYTES, server.getUri()); } - - RequestInfo req = server.getRecorder().requireRequest(); + + RequestInfo req = server.getRecorder().requireRequest(); for (Map.Entry kv: headers.entrySet()) { assertThat(req.getHeader(kv.getKey()), equalTo(kv.getValue())); } @@ -156,12 +156,12 @@ public void updateHeaders(Map h) { } }; HttpProperties httpProperties = new HttpProperties(0, headers, headersTransformer, null, null, null, 0, null, null); - + try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { try (EventSender es = makeEventSender(httpProperties)) { es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); } - + RequestInfo req = server.getRecorder().requireRequest(); assertThat(req.getHeader("name1"), equalTo("value1a")); assertThat(req.getHeader("name2"), equalTo("value2")); @@ -218,7 +218,7 @@ public void eventPayloadIdReusedOnRetry() throws Exception { assertThat(retryId, not(equalTo(payloadId))); } } - + @Test public void eventSchemaNotSetOnDiagnosticEvents() throws Exception { try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { @@ -262,7 +262,7 @@ public void http429ErrorIsRecoverable() throws Exception { public void http500ErrorIsRecoverable() throws Exception { testRecoverableHttpError(500); } - + @Test public void serverDateIsParsed() throws Exception { long fakeTime = ((new Date().getTime() - 100000) / 1000) * 1000; // don't expect millisecond precision @@ -271,7 +271,7 @@ public void serverDateIsParsed() throws Exception { try (HttpServer server = HttpServer.start(resp)) { try (EventSender es = makeEventSender()) { EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); - + assertNotNull(result.getTimeFromServer()); assertEquals(fakeTime, result.getTimeFromServer().getTime()); } @@ -285,7 +285,7 @@ public void invalidServerDateIsIgnored() throws Exception { try (HttpServer server = HttpServer.start(resp)) { try (EventSender es = makeEventSender()) { EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); - + assertTrue(result.isSuccess()); assertNull(result.getTimeFromServer()); } @@ -298,12 +298,12 @@ public void baseUriDoesNotNeedTrailingSlash() throws Exception { try (EventSender es = makeEventSender()) { URI uriWithoutSlash = URI.create(server.getUri().toString().replaceAll("/$", "")); EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, uriWithoutSlash); - + assertTrue(result.isSuccess()); assertFalse(result.isMustShutDown()); } - - RequestInfo req = server.getRecorder().requireRequest(); + + RequestInfo req = server.getRecorder().requireRequest(); assertEquals("/bulk", req.getPath()); assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); assertEquals(FAKE_DATA, req.getBody()); @@ -316,12 +316,12 @@ public void baseUriCanHaveContextPath() throws Exception { try (EventSender es = makeEventSender()) { URI baseUri = server.getUri().resolve("/context/path"); EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, baseUri); - + assertTrue(result.isSuccess()); assertFalse(result.isMustShutDown()); } - - RequestInfo req = server.getRecorder().requireRequest(); + + RequestInfo req = server.getRecorder().requireRequest(); assertEquals("/context/path/bulk", req.getPath()); assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); assertEquals(FAKE_DATA, req.getBody()); @@ -334,7 +334,7 @@ public void nothingIsSentForNullData() throws Exception { try (EventSender es = makeEventSender()) { EventSender.Result result1 = es.sendAnalyticsEvents(null, 0, server.getUri()); EventSender.Result result2 = es.sendDiagnosticEvent(null, server.getUri()); - + assertTrue(result1.isSuccess()); assertTrue(result2.isSuccess()); assertEquals(0, server.getRecorder().count()); @@ -348,41 +348,41 @@ public void nothingIsSentForEmptyData() throws Exception { try (EventSender es = makeEventSender()) { EventSender.Result result1 = es.sendAnalyticsEvents(new byte[0], 0, server.getUri()); EventSender.Result result2 = es.sendDiagnosticEvent(new byte[0], server.getUri()); - + assertTrue(result1.isSuccess()); assertTrue(result2.isSuccess()); assertEquals(0, server.getRecorder().count()); } } } - + private void testUnrecoverableHttpError(int status) throws Exception { Handler errorResponse = Handlers.status(status); - + try (HttpServer server = HttpServer.start(errorResponse)) { try (EventSender es = makeEventSender()) { EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); - + assertFalse(result.isSuccess()); assertTrue(result.isMustShutDown()); } server.getRecorder().requireRequest(); - + // it does not retry after this type of error, so there are no more requests server.getRecorder().requireNoRequests(Duration.ofMillis(100)); } } - + private void testRecoverableHttpError(int status) throws Exception { Handler errorResponse = Handlers.status(status); Handler errorsThenSuccess = Handlers.sequential(errorResponse, errorResponse, eventsSuccessResponse()); // send two errors in a row, because the flush will be retried one time - + try (HttpServer server = HttpServer.start(errorsThenSuccess)) { try (EventSender es = makeEventSender()) { EventSender.Result result = es.sendAnalyticsEvents(FAKE_DATA_BYTES, 1, server.getUri()); - + assertFalse(result.isSuccess()); assertFalse(result.isMustShutDown()); } @@ -396,7 +396,7 @@ private void testRecoverableHttpError(int status) throws Exception { private Handler eventsSuccessResponse() { return Handlers.status(202); } - + private Handler addDateHeader(Date date) { return Handlers.header("Date", httpDateFormat.format(date)); } diff --git a/src/test/java/com/launchdarkly/sdk/internal/events/EventContextFormatterTest.java b/src/test/java/com/launchdarkly/sdk/internal/events/EventContextFormatterTest.java index a241939..8e685f6 100644 --- a/src/test/java/com/launchdarkly/sdk/internal/events/EventContextFormatterTest.java +++ b/src/test/java/com/launchdarkly/sdk/internal/events/EventContextFormatterTest.java @@ -144,8 +144,8 @@ public void testOutput() throws Exception { EventContextFormatter f = new EventContextFormatter(allAttributesPrivate, globalPrivateAttributes); StringWriter sw = new StringWriter(); JsonWriter jw = new JsonWriter(sw); - - f.write(context, jw); + + f.write(context, jw, false); jw.flush(); String canonicalizedOutput = canonicalizeOutputJson(sw.toString()); diff --git a/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java b/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java index c38add5..3e08a1a 100644 --- a/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java +++ b/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java @@ -26,21 +26,19 @@ @SuppressWarnings("javadoc") public class EventOutputTest extends BaseEventTest { private static final Gson gson = new Gson(); - + private final ContextBuilder contextBuilderWithAllAttributes = LDContext.builder("userkey") - .anonymous(true) .name("me") .set("custom1", "value1") .set("custom2", "value2"); private static final LDValue contextJsonWithAllAttributes = parseValue("{" + "\"kind\":\"user\"," + "\"key\":\"userkey\"," + - "\"anonymous\":true," + "\"custom1\":\"value1\"," + "\"custom2\":\"value2\"," + "\"name\":\"me\"" + "}"); - + @Test public void allAttributesAreSerialized() throws Exception { testInlineContextSerialization(contextBuilderWithAllAttributes.build(), contextJsonWithAllAttributes, @@ -99,7 +97,7 @@ public void globalPrivateAttributeNamesMakeAttributesPrivate() throws Exception EventsConfiguration config = makeEventsConfig(false, ImmutableSet.of(AttributeRef.fromLiteral("attr1"))); testInlineContextSerialization(context, expectedJson, config); } - + @Test public void perContextPrivateAttributesMakeAttributePrivate() throws Exception { // See comment in allAttributesPrivateMakesAttributesPrivate @@ -117,19 +115,19 @@ public void perContextPrivateAttributesMakeAttributePrivate() throws Exception { EventsConfiguration config = makeEventsConfig(false, null); testInlineContextSerialization(context, expectedJson, config); } - + private ObjectBuilder buildFeatureEventProps(String key, String userKey) { return LDValue.buildObject() .put("kind", "feature") .put("key", key) .put("creationDate", 100000) - .put("contextKeys", LDValue.buildObject().put("user", userKey).build()); + .put("context", LDValue.buildObject().put("kind", "user").put("key", userKey).build()); } private ObjectBuilder buildFeatureEventProps(String key) { return buildFeatureEventProps(key, "userkey"); } - + @Test public void featureEventIsSerialized() throws Exception { LDContext context = LDContext.builder("userkey").name("me").build(); @@ -139,11 +137,12 @@ public void featureEventIsSerialized() throws Exception { FeatureRequest feWithVariation = featureEvent(context, FLAG_KEY).flagVersion(FLAG_VERSION).variation(1) .value(value).defaultValue(defaultVal).build(); LDValue feJson1 = buildFeatureEventProps(FLAG_KEY) - .put("version", FLAG_VERSION) - .put("variation", 1) - .put("value", value) - .put("default", defaultVal) - .build(); + .put("version", FLAG_VERSION) + .put("variation", 1) + .put("value", value) + .put("default", defaultVal) + .put("context", LDValue.buildObject().put("kind", "user").put("key", "userkey").put("name", "me").build()) + .build(); assertJsonEquals(feJson1, getSingleOutputEvent(f, feWithVariation)); FeatureRequest feWithoutVariationOrDefault = featureEvent(context, FLAG_KEY).flagVersion(FLAG_VERSION) @@ -151,6 +150,7 @@ public void featureEventIsSerialized() throws Exception { LDValue feJson2 = buildFeatureEventProps(FLAG_KEY) .put("version", FLAG_VERSION) .put("value", value) + .put("context", LDValue.buildObject().put("kind", "user").put("key", "userkey").put("name", "me").build()) .build(); assertJsonEquals(feJson2, getSingleOutputEvent(f, feWithoutVariationOrDefault)); @@ -162,6 +162,7 @@ public void featureEventIsSerialized() throws Exception { .put("value", value) .put("default", defaultVal) .put("reason", LDValue.buildObject().put("kind", "FALLTHROUGH").build()) + .put("context", LDValue.buildObject().put("kind", "user").put("key", "userkey").put("name", "me").build()) .build(); assertJsonEquals(feJson3, getSingleOutputEvent(f, feWithReason)); @@ -177,7 +178,7 @@ public void featureEventIsSerialized() throws Exception { .put("default", defaultVal) .build(); assertJsonEquals(feJson5, getSingleOutputEvent(f, debugEvent)); - + Event.FeatureRequest prereqEvent = featureEvent(context, FLAG_KEY).flagVersion(FLAG_VERSION) .variation(1).value(value).defaultValue(null).prereqOf("parent").build(); LDValue feJson6 = buildFeatureEventProps(FLAG_KEY) @@ -185,10 +186,68 @@ public void featureEventIsSerialized() throws Exception { .put("variation", 1) .put("value", "flagvalue") .put("prereqOf", "parent") + .put("context", LDValue.buildObject().put("kind", "user").put("key", "userkey").put("name", "me").build()) .build(); assertJsonEquals(feJson6, getSingleOutputEvent(f, prereqEvent)); } + @Test + public void featureEventRedactsAnonymousContextAttributes() throws Exception { + LDValue value = LDValue.of("flagvalue"), defaultVal = LDValue.of("defaultvalue"); + + // Single-kind context redaction + LDContext user_context = LDContext.builder("userkey").anonymous(true).name("me").set("age", 42).build(); + EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); + + FeatureRequest feWithVariation1 = featureEvent(user_context, FLAG_KEY).flagVersion(FLAG_VERSION).variation(1) + .value(value).defaultValue(defaultVal).build(); + LDValue contextJson = LDValue.buildObject() + .put("kind", "user") + .put("key", "userkey") + .put("anonymous", true) + .put("_meta", LDValue.parse("{\"redactedAttributes\":[\"name\", \"age\"]}")) + .build(); + LDValue feJson1 = buildFeatureEventProps(FLAG_KEY) + .put("version", FLAG_VERSION) + .put("variation", 1) + .put("value", value) + .put("default", defaultVal) + .put("context", contextJson) + .build(); + assertJsonEquals(feJson1, getSingleOutputEvent(f, feWithVariation1)); + + // Multi-kind context redaction + LDContext org_context = LDContext.builder("orgkey").anonymous(false).kind("org").name("me").set("age", 42).build(); + LDContext multi_context = LDContext.createMulti(user_context, org_context); + + FeatureRequest feWithVariation2 = featureEvent(multi_context, FLAG_KEY).flagVersion(FLAG_VERSION).variation(1) + .value(value).defaultValue(defaultVal).build(); + LDValue userJson = LDValue.buildObject() + .put("key", "userkey") + .put("anonymous", true) + .put("_meta", LDValue.parse("{\"redactedAttributes\":[\"name\", \"age\"]}")) + .build(); + LDValue orgJson = LDValue.buildObject() + .put("key", "orgkey") + .put("name", "me") + .put("age", 42) + .build(); + contextJson = LDValue.buildObject() + .put("kind", "multi") + .put("user", userJson) + .put("org", orgJson) + .build(); + + LDValue feJson2 = buildFeatureEventProps(FLAG_KEY) + .put("version", FLAG_VERSION) + .put("variation", 1) + .put("value", value) + .put("default", defaultVal) + .put("context", contextJson) + .build(); + assertJsonEquals(feJson2, getSingleOutputEvent(f, feWithVariation2)); + } + @Test public void identifyEventIsSerialized() throws IOException { LDContext context = LDContext.builder("userkey").name("me").build(); @@ -257,11 +316,11 @@ public void summaryEventIsSerialized() throws Exception { default1 = LDValue.of("default1"), default2 = LDValue.of("default2"), default3 = LDValue.of("default3"); LDContext context1 = LDContext.create("key1"); LDContext context2 = LDContext.createMulti(context1, LDContext.create(ContextKind.of("kind2"), "key2")); - + EventSummarizer es = new EventSummarizer(); - + es.summarizeEvent(1000, "first", 11, 1, value1a, default1, context1); // context1 has kind "user" - + es.summarizeEvent(1000, "second", 21, 1, value2a, default2, context1); es.summarizeEvent(1001, "first", 11, 1, value1a, default1, context1); @@ -304,7 +363,7 @@ public void summaryEventIsSerialized() throws Exception { parseValue("{\"value\":\"value2b\",\"variation\":2,\"version\":21,\"count\":1}"), parseValue("{\"value\":\"default2\",\"version\":21,\"count\":1}") )); - + LDValue thirdJson = featuresJson.get("third"); assertEquals("default3", thirdJson.get("default").stringValue()); assertThat(thirdJson.get("contextKinds").values(), contains(LDValue.of("user"))); @@ -621,20 +680,20 @@ public void migrationOpEventCanSerializeDifferentErrorPermutations() throws IOEx public void unknownEventClassIsNotSerialized() throws Exception { // This shouldn't be able to happen in reality. Event event = new FakeEventClass(1000, LDContext.create("user")); - + EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); StringWriter w = new StringWriter(); f.writeOutputEvents(new Event[] { event }, new EventSummary(), w); - + assertEquals("[]", w.toString()); } - + private static class FakeEventClass extends Event { public FakeEventClass(long creationDate, LDContext context) { super(creationDate, context); } } - + private static LDValue parseValue(String json) { return gson.fromJson(json, LDValue.class); } @@ -645,31 +704,32 @@ private LDValue getSingleOutputEvent(EventOutputFormatter f, Event event) throws assertEquals(1, count); return parseValue(w.toString()).get(0); } - + private void testContextKeysSerialization(LDContext context, LDValue expectedJsonValue) throws IOException { EventsConfiguration config = makeEventsConfig(false, null); EventOutputFormatter f = new EventOutputFormatter(config); - - Event.FeatureRequest featureEvent = featureEvent(context, FLAG_KEY).build(); - LDValue outputEvent = getSingleOutputEvent(f, featureEvent); - assertJsonEquals(expectedJsonValue, outputEvent.get("contextKeys")); - assertJsonEquals(LDValue.ofNull(), outputEvent.get("context")); - + Event.Custom customEvent = customEvent(context, "eventkey").build(); - outputEvent = getSingleOutputEvent(f, customEvent); + LDValue outputEvent = getSingleOutputEvent(f, customEvent); assertJsonEquals(expectedJsonValue, outputEvent.get("contextKeys")); assertJsonEquals(LDValue.ofNull(), outputEvent.get("context")); } - + private void testInlineContextSerialization(LDContext context, LDValue expectedJsonValue, EventsConfiguration baseConfig) throws IOException { EventsConfiguration config = makeEventsConfig(baseConfig.allAttributesPrivate, baseConfig.privateAttributes); EventOutputFormatter f = new EventOutputFormatter(config); + Event.FeatureRequest featureEvent = featureEvent(context, FLAG_KEY).build(); + LDValue outputEvent = getSingleOutputEvent(f, featureEvent); + assertJsonEquals(LDValue.ofNull(), outputEvent.get("contextKeys")); + assertJsonEquals(expectedJsonValue, outputEvent.get("context")); + + Event.Identify identifyEvent = identifyEvent(context); - LDValue outputEvent = getSingleOutputEvent(f, identifyEvent); + outputEvent = getSingleOutputEvent(f, identifyEvent); assertJsonEquals(LDValue.ofNull(), outputEvent.get("contextKeys")); assertJsonEquals(expectedJsonValue, outputEvent.get("context")); - + Event.Index indexEvent = new Event.Index(0, context); outputEvent = getSingleOutputEvent(f, indexEvent); assertJsonEquals(LDValue.ofNull(), outputEvent.get("contextKeys"));