Skip to content
Draft
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 @@ -41,10 +41,10 @@ internal fun <T : Any> ResolutionDetails<T>.toProviderEvaluation(): ProviderEval
reason = this.reason?.name,
errorCode = this.errorCode?.toOpenFeatureErrorCode(),
errorMessage = this.errorMessage,
metadata = this.flagMetadata.toEvaluationMetadata()
metadata = flagMetadata.toEvaluationMetadata(allocationKey)
)

private fun Map<String, Any>.toEvaluationMetadata(): EvaluationMetadata {
private fun Map<String, Any>.toEvaluationMetadata(allocationKey: String? = null): EvaluationMetadata {
val builder = Builder()
forEach { (key, value) ->
when (value) {
Expand All @@ -55,6 +55,7 @@ private fun Map<String, Any>.toEvaluationMetadata(): EvaluationMetadata {
else -> builder.putString(key, value.toString())
}
}
allocationKey?.let { builder.putString("allocationKey", it) }
return builder.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,15 @@ internal class ConvertersTest {
}

@Test
fun `M surface allocationKey in metadata W toProviderEvaluation() {flagMetadata contains allocationKey}`(
@BoolForgery fakeValue: Boolean,
@StringForgery fakeAllocationKey: String
fun `M surface allocationKey in metadata W toProviderEvaluation() {resolution with allocationKey}`(
@StringForgery fakeAllocationKey: String,
@BoolForgery fakeValue: Boolean
) {
// Given
val resolution = ResolutionDetails(
value = fakeValue,
reason = ResolutionReason.TARGETING_MATCH,
flagMetadata = mapOf("allocationKey" to fakeAllocationKey)
allocationKey = fakeAllocationKey
)

// When
Expand Down Expand Up @@ -173,6 +173,66 @@ internal class ConvertersTest {
assertThat(result.metadata.getString("count")).isEqualTo("42")
}

@Test
fun `M not surface allocationKey in metadata W toProviderEvaluation() {resolution with null allocationKey}`(
@BoolForgery fakeValue: Boolean
) {
// Given
val resolution = ResolutionDetails(
value = fakeValue,
reason = ResolutionReason.DEFAULT,
allocationKey = null
)

// When
val result = resolution.toProviderEvaluation()

// Then
assertThat(result.metadata.getString("allocationKey")).isNull()
}

@Test
fun `M surface allocationKey and flagMetadata W toProviderEvaluation() {both present}`(
@StringForgery fakeAllocationKey: String,
@BoolForgery fakeValue: Boolean
) {
// Given
val resolution = ResolutionDetails(
value = fakeValue,
reason = ResolutionReason.TARGETING_MATCH,
allocationKey = fakeAllocationKey,
flagMetadata = mapOf("env" to "prod")
)

// When
val result = resolution.toProviderEvaluation()

// Then — allocationKey and flagMetadata entries both surfaced
assertThat(result.metadata.getString("allocationKey")).isEqualTo(fakeAllocationKey)
assertThat(result.metadata.getString("env")).isEqualTo("prod")
}

@Test
fun `M typed allocationKey wins W toProviderEvaluation() {flagMetadata also contains allocationKey}`(
@StringForgery fakeAllocationKey: String,
@StringForgery fakeMetadataAllocationKey: String,
@BoolForgery fakeValue: Boolean
) {
// Given — flagMetadata has an "allocationKey" entry that should be overridden by the typed field
val resolution = ResolutionDetails(
value = fakeValue,
reason = ResolutionReason.TARGETING_MATCH,
allocationKey = fakeAllocationKey,
flagMetadata = mapOf("allocationKey" to fakeMetadataAllocationKey)
)

// When
val result = resolution.toProviderEvaluation()

// Then — typed allocationKey field wins over flagMetadata entry
assertThat(result.metadata.getString("allocationKey")).isEqualTo(fakeAllocationKey)
}

// endregion

// region toOpenFeatureErrorCode
Expand Down
2 changes: 1 addition & 1 deletion features/dd-sdk-android-flags/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ sealed class com.datadog.android.flags.model.FlagsClientState
data class Error : FlagsClientState
constructor(Throwable? = null)
data class com.datadog.android.flags.model.ResolutionDetails<T: Any>
constructor(T, String? = null, ResolutionReason? = null, ErrorCode? = null, String? = null, Map<String, Any> = emptyMap())
constructor(T, String? = null, ResolutionReason? = null, ErrorCode? = null, String? = null, String? = null, Map<String, Any> = emptyMap())
enum com.datadog.android.flags.model.ResolutionReason
- STATIC
- DEFAULT
Expand Down
12 changes: 7 additions & 5 deletions features/dd-sdk-android-flags/api/dd-sdk-android-flags.api
Original file line number Diff line number Diff line change
Expand Up @@ -457,17 +457,19 @@ public final class com/datadog/android/flags/model/FlagsClientState$Stale : com/
}

public final class com/datadog/android/flags/model/ResolutionDetails {
public fun <init> (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Lcom/datadog/android/flags/model/ResolutionReason;
public final fun component4 ()Lcom/datadog/android/flags/model/ErrorCode;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/util/Map;
public final fun copy (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/flags/model/ResolutionDetails;
public static synthetic fun copy$default (Lcom/datadog/android/flags/model/ResolutionDetails;Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/flags/model/ResolutionDetails;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Ljava/util/Map;
public final fun copy (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/flags/model/ResolutionDetails;
public static synthetic fun copy$default (Lcom/datadog/android/flags/model/ResolutionDetails;Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/flags/model/ResolutionDetails;
public fun equals (Ljava/lang/Object;)Z
public final fun getAllocationKey ()Ljava/lang/String;
public final fun getErrorCode ()Lcom/datadog/android/flags/model/ErrorCode;
public final fun getErrorMessage ()Ljava/lang/String;
public final fun getFlagMetadata ()Ljava/util/Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ internal class DatadogFlagsClient(
reason = parseReason(precomputedFlag.reason),
errorCode = null,
errorMessage = null,
allocationKey = precomputedFlag.allocationKey.takeIf { it.isNotBlank() },
flagMetadata = buildMetadata(precomputedFlag)
)

Expand All @@ -409,9 +410,6 @@ internal class DatadogFlagsClient(
is String, is Number, is Boolean -> metadata[key] = value
}
}
if (precomputedFlag.allocationKey.isNotBlank()) {
metadata["allocationKey"] = precomputedFlag.allocationKey
}
return metadata
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.datadog.android.flags.model
* @property reason Optional reason code explaining why this value was resolved.
* @property errorCode Optional error code if the resolution failed. Null indicates successful resolution.
* @property errorMessage Optional human-readable error message providing additional context about failures.
* @property allocationKey The allocation key from the flag assignment. Null if not available.
* @property flagMetadata Map of arbitrary metadata associated with the flag (string keys, primitive values). Empty if no metadata.
*/
data class ResolutionDetails<T : Any>(
Expand All @@ -26,5 +27,6 @@ data class ResolutionDetails<T : Any>(
val reason: ResolutionReason? = null,
val errorCode: ErrorCode? = null,
val errorMessage: String? = null,
val allocationKey: String? = null,
val flagMetadata: Map<String, Any> = emptyMap()
)
Original file line number Diff line number Diff line change
Expand Up @@ -832,42 +832,12 @@ internal class DatadogFlagsClientTest {
assertThat(result.reason).isEqualTo(ResolutionReason.valueOf(fakeReason))
assertThat(result.errorCode).isNull()
assertThat(result.errorMessage).isNull()
assertThat(result.flagMetadata).isNotNull
assertThat(result.allocationKey).isEqualTo(fakeAllocationKey)
assertThat(result.flagMetadata).containsKeys("version", "environment")
assertThat(result.flagMetadata["allocationKey"]).isEqualTo(fakeAllocationKey)
}

@Test
fun `M typed allocationKey wins W resolve() { extraLogging also contains allocationKey }`(forge: Forge) {
// Given
val fakeFlagKey = forge.anAlphabeticalString()
val fakeDefaultValue = forge.aBool()
val fakeFlagValue = !fakeDefaultValue
val fakeAllocationKey = forge.anAlphabeticalString()
val fakeExtraLoggingAllocationKey = forge.anAlphabeticalString()
val fakeFlag = forge.getForgery<PrecomputedFlag>().copy(
variationType = VariationType.BOOLEAN.value,
variationValue = fakeFlagValue.toString(),
allocationKey = fakeAllocationKey,
extraLogging = JSONObject().apply {
put("allocationKey", fakeExtraLoggingAllocationKey)
}
)
val fakeContext = EvaluationContext(
targetingKey = forge.anAlphabeticalString(),
attributes = emptyMap()
)
whenever(mockFlagsRepository.getPrecomputedFlagWithContext(fakeFlagKey)) doReturn (fakeFlag to fakeContext)

// When
val result = testedClient.resolve(fakeFlagKey, fakeDefaultValue)

// Then - typed allocationKey wins over any "allocationKey" entry from extraLogging
assertThat(result.flagMetadata["allocationKey"]).isEqualTo(fakeAllocationKey)
}

@Test
fun `M allocationKey excluded from metadata W resolve() { empty allocationKey }`(forge: Forge) {
fun `M allocationKey null W resolve() { empty allocationKey }`(forge: Forge) {
// Given
val fakeFlagKey = forge.anAlphabeticalString()
val fakeDefaultValue = forge.aBool()
Expand All @@ -889,11 +859,11 @@ internal class DatadogFlagsClientTest {

// Then
assertThat(result.value).isEqualTo(fakeFlagValue)
assertThat(result.flagMetadata).doesNotContainKey("allocationKey")
assertThat(result.allocationKey).isNull()
}

@Test
fun `M allocationKey excluded from metadata W resolve() { whitespace allocationKey }`(forge: Forge) {
fun `M allocationKey null W resolve() { whitespace allocationKey }`(forge: Forge) {
// Given
val fakeFlagKey = forge.anAlphabeticalString()
val fakeDefaultValue = forge.aBool()
Expand All @@ -915,7 +885,7 @@ internal class DatadogFlagsClientTest {

// Then
assertThat(result.value).isEqualTo(fakeFlagValue)
assertThat(result.flagMetadata).doesNotContainKey("allocationKey")
assertThat(result.allocationKey).isNull()
}

@Test
Expand Down Expand Up @@ -1004,6 +974,7 @@ internal class DatadogFlagsClientTest {
assertThat(result.errorCode).isEqualTo(ErrorCode.TYPE_MISMATCH)
assertThat(result.errorMessage).contains("Flag '$fakeFlagKey'")
assertThat(result.errorMessage).contains("has type 'string' but Boolean was requested")
assertThat(result.allocationKey).isNull()
assertThat(result.flagMetadata).isEmpty()

// Verify no exposure tracked for type mismatch
Expand All @@ -1027,6 +998,7 @@ internal class DatadogFlagsClientTest {
assertThat(result.errorCode).isEqualTo(ErrorCode.FLAG_NOT_FOUND)
assertThat(result.errorMessage).contains("Flag '$fakeFlagKey'")
assertThat(result.errorMessage).contains("Flag not found")
assertThat(result.allocationKey).isNull()
assertThat(result.flagMetadata).isEmpty()

// Verify no exposure tracked when flag not found
Expand All @@ -1050,6 +1022,7 @@ internal class DatadogFlagsClientTest {
assertThat(result.errorCode).isEqualTo(ErrorCode.PROVIDER_NOT_READY)
assertThat(result.errorMessage).contains("Flag '$fakeFlagKey'")
assertThat(result.errorMessage).contains("Provider not ready")
assertThat(result.allocationKey).isNull()
assertThat(result.flagMetadata).isEmpty()

// Verify no exposure tracked when provider not ready
Expand Down Expand Up @@ -1084,6 +1057,7 @@ internal class DatadogFlagsClientTest {
assertThat(result.errorCode).isEqualTo(ErrorCode.PARSE_ERROR)
assertThat(result.errorMessage).contains("Flag '$fakeFlagKey'")
assertThat(result.errorMessage).contains("Failed to parse value")
assertThat(result.allocationKey).isNull()
assertThat(result.flagMetadata).isEmpty()

// Verify no exposure tracked for parse error
Expand Down
Loading