Skip to content
Merged
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
3 changes: 3 additions & 0 deletions lib/sdk/server/contract-tests/test-suppressions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
events/custom events/single-kind anonymous context redacts all attributes
events/custom events/multi-kind with anonymous context redacts attributes appropriately
migrations/redacts anonymous context attributes
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
*/
final class EventOutputFormatter {
private final EventContextFormatter contextFormatter;
private final boolean redactAnonymousAllEvents;

EventOutputFormatter(EventsConfiguration config) {
this.contextFormatter = new EventContextFormatter(
config.allAttributesPrivate,
config.privateAttributes.toArray(new AttributeRef[config.privateAttributes.size()]));
this.redactAnonymousAllEvents = config.redactAnonymousAllEvents;
}

int writeOutputEvents(Event[] events, List<EventSummarizer.EventSummary> summaries, Writer writer) throws IOException {
Expand Down Expand Up @@ -92,7 +94,11 @@ private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException
jw.beginObject();
writeKindAndCreationDate(jw, "custom", event.getCreationDate());
jw.name("key").value(ce.getKey());
writeContext(ce.getContext(), jw, false);
// Anonymous-context attribute redaction in custom events is server-side SDK behavior
// (redactAnonymousAllEvents): server-side SDKs inline the full context and redact, while
// client-side SDKs use the current context and do not redact (only feature events redact
// anonymous on the client).
writeContext(ce.getContext(), jw, redactAnonymousAllEvents);
writeLDValue("data", ce.getData(), jw);
if (ce.getMetricValue() != null) {
jw.name("metricValue");
Expand All @@ -107,7 +113,9 @@ private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException
} else if (event instanceof Event.MigrationOp) {
jw.beginObject();
writeKindAndCreationDate(jw, "migration_op", event.getCreationDate());
writeContext(event.getContext(), jw, false);
// migration_op events are only produced by server-side SDKs, which redact anonymous context
// attributes (redactAnonymousAllEvents), consistent with their custom-event handling.
writeContext(event.getContext(), jw, redactAnonymousAllEvents);

Event.MigrationOp me = (Event.MigrationOp)event;
jw.name("operation").value(me.getOperation());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public final class EventsConfiguration {
final boolean initiallyOffline;
final List<AttributeRef> privateAttributes;
final boolean perContextSummarization;

final boolean redactAnonymousAllEvents;

/**
* Creates an instance.
*
Expand Down Expand Up @@ -103,6 +104,49 @@ public EventsConfiguration(
Collection<AttributeRef> privateAttributes,
boolean perContextSummarization
) {
this(allAttributesPrivate, capacity, contextDeduplicator, diagnosticRecordingIntervalMillis,
diagnosticStore, eventSender, eventSendingThreadPoolSize, eventsUri, flushIntervalMillis,
initiallyInBackground, initiallyOffline, privateAttributes, perContextSummarization, false);
}

/**
* Creates an instance.
*
* @param allAttributesPrivate true if all attributes are private
* @param capacity event buffer capacity (if zero or negative, a value of 1 is used to prevent errors)
* @param contextDeduplicator optional EventContextDeduplicator; null for client-side SDK
* @param diagnosticRecordingIntervalMillis diagnostic recording interval
* @param diagnosticStore optional DiagnosticStore; null if diagnostics are disabled
* @param eventSender event delivery component; must not be null
* @param eventSendingThreadPoolSize number of worker threads for event delivery; zero to use the default
* @param eventsUri events base URI
* @param flushIntervalMillis event flush interval
* @param initiallyInBackground true if we should start out in background mode (see
* {@link DefaultEventProcessor#setInBackground(boolean)})
* @param initiallyOffline true if we should start out in offline mode (see
* {@link DefaultEventProcessor#setOffline(boolean)})
* @param privateAttributes list of private attribute references; may be null
* @param perContextSummarization true to generate separate summary events per context
* @param redactAnonymousAllEvents true to redact anonymous context attributes across all inlined
* events (custom and migration_op events), in addition to feature events which always redact them.
* Server-side SDKs set this to true; client-side SDKs leave it false (redact only feature events).
*/
public EventsConfiguration(
boolean allAttributesPrivate,
int capacity,
EventContextDeduplicator contextDeduplicator,
long diagnosticRecordingIntervalMillis,
DiagnosticStore diagnosticStore,
EventSender eventSender,
int eventSendingThreadPoolSize,
URI eventsUri,
long flushIntervalMillis,
boolean initiallyInBackground,
boolean initiallyOffline,
Collection<AttributeRef> privateAttributes,
boolean perContextSummarization,
boolean redactAnonymousAllEvents
) {
super();
this.allAttributesPrivate = allAttributesPrivate;
this.capacity = capacity >= 0 ? capacity : 1;
Expand All @@ -118,5 +162,6 @@ public EventsConfiguration(
this.initiallyOffline = initiallyOffline;
this.privateAttributes = privateAttributes == null ? Collections.emptyList() : new ArrayList<>(privateAttributes);
this.perContextSummarization = perContextSummarization;
this.redactAnonymousAllEvents = redactAnonymousAllEvents;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ public static class EventsConfigurationBuilder {
private boolean initiallyOffline = false;
private Set<AttributeRef> privateAttributes = new HashSet<>();
private boolean perContextSummarization = false;
private boolean redactAnonymousAllEvents = false;

public EventsConfiguration build() {
return new EventsConfiguration(
Expand All @@ -349,10 +350,16 @@ public EventsConfiguration build() {
initiallyInBackground,
initiallyOffline,
privateAttributes,
perContextSummarization
perContextSummarization,
redactAnonymousAllEvents
);
}

public EventsConfigurationBuilder redactAnonymousAllEvents(boolean redactAnonymousAllEvents) {
this.redactAnonymousAllEvents = redactAnonymousAllEvents;
return this;
}

public EventsConfigurationBuilder allAttributesPrivate(boolean allAttributesPrivate) {
this.allAttributesPrivate = allAttributesPrivate;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,114 @@ public void customEventIsSerialized() throws IOException {
assertJsonEquals(ceJson4, getSingleOutputEvent(f, ceWithDataAndMetric));
}

@Test
public void customEventRedactsAnonymousContextAttributes() throws Exception {
// Server-side SDK behavior: redactAnonymousAllEvents is enabled.
EventOutputFormatter f = new EventOutputFormatter(
new EventsConfigurationBuilder().redactAnonymousAllEvents(true).build());

// Single-kind anonymous context: every optional attribute is redacted.
LDContext userContext = LDContext.builder("userkey").anonymous(true).name("me").set("age", 42).build();
Event.Custom singleKind = customEvent(userContext, "customkey").build();
LDValue singleContextJson = LDValue.buildObject()
.put("kind", "user")
.put("key", "userkey")
.put("anonymous", true)
.put("_meta", LDValue.parse("{\"redactedAttributes\":[\"name\", \"age\"]}"))
.build();
LDValue singleJson = parseValue("{" +
"\"kind\":\"custom\"," +
"\"creationDate\":100000," +
"\"key\":\"customkey\"," +
"\"context\":" + singleContextJson +
"}");
assertJsonEquals(singleJson, getSingleOutputEvent(f, singleKind));

// Multi-kind context: only the anonymous kind (user) is redacted; org is untouched.
LDContext orgContext = LDContext.builder("orgkey").anonymous(false).kind("org").name("me").set("age", 42).build();
LDContext multiContext = LDContext.createMulti(userContext, orgContext);
Event.Custom multiKind = customEvent(multiContext, "customkey").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();
LDValue multiContextJson = LDValue.buildObject()
.put("kind", "multi")
.put("user", userJson)
.put("org", orgJson)
.build();
LDValue multiJson = parseValue("{" +
"\"kind\":\"custom\"," +
"\"creationDate\":100000," +
"\"key\":\"customkey\"," +
"\"context\":" + multiContextJson +
"}");
assertJsonEquals(multiJson, getSingleOutputEvent(f, multiKind));
}

@Test
public void customEventDoesNotRedactAnonymousContextWhenNotEnabled() throws Exception {
// Client-side SDK behavior: with redactAnonymousAllEvents left false (the
// default), custom events carry the full anonymous context, including its attributes.
EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig());

LDContext userContext = LDContext.builder("userkey").anonymous(true).name("me").set("age", 42).build();
Event.Custom ce = customEvent(userContext, "customkey").build();
LDValue contextJson = LDValue.buildObject()
.put("kind", "user")
.put("key", "userkey")
.put("anonymous", true)
.put("name", "me")
.put("age", 42)
.build();
LDValue ceJson = parseValue("{" +
"\"kind\":\"custom\"," +
"\"creationDate\":100000," +
"\"key\":\"customkey\"," +
"\"context\":" + contextJson +
"}");
assertJsonEquals(ceJson, getSingleOutputEvent(f, ce));
}

@Test
public void migrationOpEventRedactsAnonymousContextAttributes() throws Exception {
// Server-side SDK behavior: redactAnonymousAllEvents is enabled (migration_op events are
// only produced server-side).
EventOutputFormatter f = new EventOutputFormatter(
new EventsConfigurationBuilder().redactAnonymousAllEvents(true).build());

LDContext userContext = LDContext.builder("userkey").anonymous(true).name("me").set("age", 42).build();
Event.MigrationOp migrationEvent = new Event.MigrationOp(
100000,
userContext,
"migration-key",
1,
2,
LDValue.of("live"),
LDValue.of("off"),
EvaluationReason.fallthrough(false),
1,
"read",
new Event.MigrationOp.InvokedMeasurement(true, false),
null,
null,
null
);
LDValue expectedContext = LDValue.buildObject()
.put("kind", "user")
.put("key", "userkey")
.put("anonymous", true)
.put("_meta", LDValue.parse("{\"redactedAttributes\":[\"name\", \"age\"]}"))
.build();
assertJsonEquals(expectedContext, getSingleOutputEvent(f, migrationEvent).get("context"));
}

@Test
public void summaryEventIsSerialized() throws Exception {
LDValue value1a = LDValue.of("value1a"), value2a = LDValue.of("value2a"), value2b = LDValue.of("value2b"),
Expand Down
Loading