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
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,14 @@ private void trackLocalExposure(Map<String, Object> context, String flagKey, Str

Object distinctIdObj = context.get("distinct_id");
if (distinctIdObj == null) {
// Local eval succeeds when the flag's Variant Assignment Key is
// something other than distinct_id (e.g., device_id), but the
// exposure event still needs distinct_id to attribute the user.
// Surface the drop instead of silently returning so callers can
// see they need to include distinct_id in the context.
logger.log(Level.WARNING,
"Cannot track exposure event for flag ''{0}'' without a distinct_id in the context",
flagKey);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,58 @@ public void testDoNotTrackExposureWhenDistinctIdIsMissing() {
assertEquals(0, eventSender.getEvents().size());
}

// SDK-84: when the flag is bucketed on a non-distinct_id key (e.g.,
// device_id) and the context has that key but not distinct_id, local
// eval succeeds — but exposure still needs distinct_id to attribute the
// user. Surface the drop via a warning log instead of silently returning.
@Test
public void testWarnsWhenExposureDroppedDueToMissingDistinctId() throws Exception {
List<Variant> variants = Arrays.asList(new Variant("variant-a", "value-a", false, 1.0f));
List<Rollout> rollouts = Arrays.asList(new Rollout(1.0f));
// Flag is bucketed on device_id, not distinct_id.
String response = buildFlagsResponse("device-flag", "device_id", variants, rollouts, null);

provider = createProviderWithResponse(response);
provider.startPollingForDefinitions();

// Capture log records emitted from LocalFlagsProvider.
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(LocalFlagsProvider.class.getName());
java.util.List<java.util.logging.LogRecord> records = new java.util.ArrayList<>();
java.util.logging.Handler handler = new java.util.logging.Handler() {
public void publish(java.util.logging.LogRecord record) { records.add(record); }
public void flush() {}
public void close() {}
};
handler.setLevel(java.util.logging.Level.ALL);
java.util.logging.Level originalLevel = logger.getLevel();
logger.addHandler(handler);
logger.setLevel(java.util.logging.Level.ALL);

try {
Comment thread
tylerjroach marked this conversation as resolved.
Map<String, Object> context = new HashMap<>();
context.put("device_id", "device-abc");

Object result = provider.getVariantValue("device-flag", fallbackVariantValue, context);

// Eval still succeeds — caller gets the real variant value.
assertEquals("value-a", result);
// ...but exposure is dropped because distinct_id is missing.
assertEquals(0, eventSender.getEvents().size());
// And we now log a warning so the drop isn't silent. Use a
// formatter so we can match against the fully-resolved
// template (the raw LogRecord.getMessage() still contains {0}).
java.util.logging.SimpleFormatter formatter = new java.util.logging.SimpleFormatter();
boolean warned = records.stream()
.filter(r -> r.getLevel().intValue() >= java.util.logging.Level.WARNING.intValue())
.map(formatter::formatMessage)
.anyMatch(m -> m.contains("device-flag") && m.contains("distinct_id"));
assertTrue("expected a warning about the dropped exposure event", warned);
} finally {
logger.removeHandler(handler);
logger.setLevel(originalLevel);
}
}

// #endregion
// #region Readiness Tests

Expand Down
Loading