feat: context management#2886
Conversation
| } | ||
|
|
||
| internal fun build( | ||
| site: String, |
There was a problem hiding this comment.
Users will get these parameters from here or from datadogContext?
| updatedAttributes[key] = value | ||
| setContext(currentContext.targetingKey, updatedAttributes) | ||
| } else { | ||
| throw IllegalStateException( |
There was a problem hiding this comment.
Do we want to throw an exception here?
| context.targetingAttributes.forEach { (key, value) -> | ||
| when (value) { | ||
| is String -> attributesJson.put(key, value) | ||
| is Number -> attributesJson.put(key, value) | ||
| is Boolean -> attributesJson.put(key, value) | ||
| is Collection<*> -> { | ||
| // Handle arrays/lists by converting to JSON array | ||
| val jsonArray = org.json.JSONArray(value) | ||
| attributesJson.put(key, jsonArray) | ||
| } | ||
| is Map<*, *> -> { | ||
| // Handle nested objects | ||
| val jsonObject = JSONObject(value as Map<String, Any>) | ||
| attributesJson.put(key, jsonObject) | ||
| } | ||
| else -> attributesJson.put(key, value.toString()) | ||
| } | ||
| } |
There was a problem hiding this comment.
question/ It looks like we allow multiple data types in targeting_attributes, but the endpoint spec defines them as Map<String, String>:
targeting_attributes:
type: object
description: Key/value attributes for subject targeting.
additionalProperties:
type: string
example:
attr1: value1
companyId: "1"
The js-client stringifies targeting_attributes, and @sameerank did the same in the Swift client here.
To better support this in a typed language like Swift, we decided to require a [String: String] map in the EvaluationContext API. This could be relaxed later (by stringifying various types internally), but the plan is to handle that in future iterations.
Just double-checking here—this implementation doesn’t seem consistent with the spec. Is that intentional?
There was a problem hiding this comment.
not intentional - got carried away a bit; have fixed!
5601928 to
d8c2c35
Compare
| data class com.datadog.android.flags.featureflags.model.EvaluationContext | ||
| constructor(String, Map<String, Any> = emptyMap()) | ||
| class Builder | ||
| constructor(String, com.datadog.android.api.InternalLogger) | ||
| fun addAttribute(String, String): Builder | ||
| fun addAttribute(String, Number): Builder | ||
| fun addAttribute(String, Boolean): Builder | ||
| fun addAttribute(String, Any): Builder | ||
| fun addAll(Map<String, Any>): Builder | ||
| fun build(): EvaluationContext | ||
| companion object | ||
| fun builder(String, com.datadog.android.api.InternalLogger) |
There was a problem hiding this comment.
We shouldn't expose InternalLogger in the public API, it is not supposed to be used by the customer.
Is this class EvaluationContext should be internal and made public by mistake?
There was a problem hiding this comment.
The EvaluationContext is going to be an external model, so I've split it out now, with an internal model where we can do validation/filtering.
| @@ -9,12 +9,24 @@ object com.datadog.android.flags.featureflags.FlagsClient | |||
| fun isRegistered(com.datadog.android.api.SdkCore = Datadog.getInstance()): Boolean | |||
| fun get(com.datadog.android.api.SdkCore = Datadog.getInstance()): FlagsProvider | |||
| interface com.datadog.android.flags.featureflags.FlagsProvider | |||
There was a problem hiding this comment.
What is the reason to have flags.featureflags in the package name? Shouldn't it be only flags or featureflags? Using both seems redundant.
There was a problem hiding this comment.
I do agree we need to settle on a single name here. I think flags is sufficient. Because we're two working on a lot of the same files right now, we'll make this rename in this feature branch in an independent pr
| /** | ||
| * Set the context for the provider. | ||
| * @param newContext The new context to use for the provider. | ||
| * @param targetingKey The targeting key to use for flag evaluation. |
There was a problem hiding this comment.
We probably need better documentation for that, it is not clear what is targetingKey by looking just on the javadoc.
There was a problem hiding this comment.
Have filled out the docs on the EvaluationContext for targetingKey and have updated the signature here.
| * @param attributes The attributes to use for targeting. | ||
| */ | ||
| fun setContext(newContext: ProviderContext) | ||
| fun setContext(targetingKey: String, attributes: Map<String, Any>) |
There was a problem hiding this comment.
Usually the type of attributes (at least in RUM and Logs) is Map<String, Any?> (allowing nullable value). Is there a reason for using non-nullable value type?
There was a problem hiding this comment.
We don't have targeting for null attributes at this time, but it's possible we will in the future. I've refactored the EvaluationContext to an internal and external model where the former will carry only the attributes supported by the flagging system and the external model is more freeform and doesn't use the internal logger
| ) | ||
| } | ||
|
|
||
| // Create orchestrator with network manager and dependencies |
There was a problem hiding this comment.
nit: no need to have comments like that, they don't add any value
| verify(mockInternalLogger, times(3)).log( | ||
| eq(InternalLogger.Level.WARN), | ||
| eq(InternalLogger.Target.USER), | ||
| any(), | ||
| eq(null), | ||
| eq(false), | ||
| eq(null) | ||
| ) |
There was a problem hiding this comment.
We are checking only logging and we don't check that we have proper attributes as a result of filtering.
If underlying code will filter out valid attributes and keep invalid, this test will still pass.
You need to assert that only validAttributes are kept.
There was a problem hiding this comment.
this test is moved to DatadogEvaluationContextTest
|
|
||
| // Then | ||
| // Should complete without errors and not log any warnings | ||
| verify(mockInternalLogger, times(0)).log( |
There was a problem hiding this comment.
same: we check only logging here, but not checking that attributes didn't make it.
| @Test | ||
| fun `M create context W constructor() { targeting key and attributes }`(forge: Forge) { | ||
| // Given | ||
| val fakeTargetingKey = forge.anAlphabeticalString() | ||
| val fakeAttributes = mapOf( | ||
| "plan" to forge.anAlphabeticalString(), | ||
| "user_id" to forge.anInt() | ||
| ) | ||
|
|
||
| // When | ||
| val context = EvaluationContext(fakeTargetingKey, fakeAttributes) | ||
|
|
||
| // Then | ||
| assertThat(context.targetingKey).isEqualTo(fakeTargetingKey) | ||
| assertThat(context.attributes).isEqualTo(fakeAttributes) | ||
| } |
There was a problem hiding this comment.
not particularly, I suppose
| @BeforeEach | ||
| fun `set up`(forge: Forge) { | ||
| ForgeConfigurator.configure(forge) | ||
| } |
There was a problem hiding this comment.
this can be replace by @ForgeConfiguration(ForgeConfigurator::class) annotation on the class
There was a problem hiding this comment.
🧑🍳 thanks. much cleaner!
| verify(mockInternalLogger, times(0)).log( | ||
| any<InternalLogger.Level>(), | ||
| any<InternalLogger.Target>(), | ||
| any<() -> String>(), | ||
| any(), | ||
| any(), | ||
| any() | ||
| ) |
There was a problem hiding this comment.
verifyNoInteractions(mockInternalLogger), same for similar blocks below
| @BeforeEach | ||
| fun `set up`(forge: Forge) { | ||
| ForgeConfigurator.configure(forge) | ||
| } |
There was a problem hiding this comment.
🧑🍳 thanks. much cleaner!
| data class com.datadog.android.flags.featureflags.model.EvaluationContext | ||
| constructor(String, Map<String, Any> = emptyMap()) | ||
| class Builder | ||
| constructor(String, com.datadog.android.api.InternalLogger) | ||
| fun addAttribute(String, String): Builder | ||
| fun addAttribute(String, Number): Builder | ||
| fun addAttribute(String, Boolean): Builder | ||
| fun addAttribute(String, Any): Builder | ||
| fun addAll(Map<String, Any>): Builder | ||
| fun build(): EvaluationContext | ||
| companion object | ||
| fun builder(String, com.datadog.android.api.InternalLogger) |
There was a problem hiding this comment.
The EvaluationContext is going to be an external model, so I've split it out now, with an internal model where we can do validation/filtering.
| /** | ||
| * Set the context for the provider. | ||
| * @param newContext The new context to use for the provider. | ||
| * @param targetingKey The targeting key to use for flag evaluation. |
There was a problem hiding this comment.
Have filled out the docs on the EvaluationContext for targetingKey and have updated the signature here.
| verify(mockInternalLogger, times(0)).log( | ||
| any<InternalLogger.Level>(), | ||
| any<InternalLogger.Target>(), | ||
| any<() -> String>(), | ||
| any(), | ||
| any(), | ||
| any() | ||
| ) |
| @Test | ||
| fun `M create context W constructor() { targeting key and attributes }`(forge: Forge) { | ||
| // Given | ||
| val fakeTargetingKey = forge.anAlphabeticalString() | ||
| val fakeAttributes = mapOf( | ||
| "plan" to forge.anAlphabeticalString(), | ||
| "user_id" to forge.anInt() | ||
| ) | ||
|
|
||
| // When | ||
| val context = EvaluationContext(fakeTargetingKey, fakeAttributes) | ||
|
|
||
| // Then | ||
| assertThat(context.targetingKey).isEqualTo(fakeTargetingKey) | ||
| assertThat(context.attributes).isEqualTo(fakeAttributes) | ||
| } |
There was a problem hiding this comment.
not particularly, I suppose
|
|
||
| // Then | ||
| // Should complete without errors and not log any warnings | ||
| verify(mockInternalLogger, times(0)).log( |
| @@ -9,12 +9,24 @@ object com.datadog.android.flags.featureflags.FlagsClient | |||
| fun isRegistered(com.datadog.android.api.SdkCore = Datadog.getInstance()): Boolean | |||
| fun get(com.datadog.android.api.SdkCore = Datadog.getInstance()): FlagsProvider | |||
| interface com.datadog.android.flags.featureflags.FlagsProvider | |||
There was a problem hiding this comment.
I do agree we need to settle on a single name here. I think flags is sufficient. Because we're two working on a lot of the same files right now, we'll make this rename in this feature branch in an independent pr
| ) | ||
| } | ||
|
|
||
| // Create orchestrator with network manager and dependencies |
|
|
||
| // Pass to orchestrator to handle network request and atomic storage | ||
| flagsOrchestrator.updateEvaluationsForContext(evaluationContext) | ||
| } catch (e: IllegalArgumentException) { |
There was a problem hiding this comment.
I've shifted around the validation here to avoid the exception, thank you
| // Since orchestrator runs async, we can't directly verify the EvaluationContext | ||
| // But we can verify that the method completes without throwing |
There was a problem hiding this comment.
hoisted the evaluations manager so we can mock it; makes this a lot less awkward
Feature Flags Evaluat Context Implementation
What does this PR do?
Implements synchronous evaluation context management for feature flag evaluation, inclusion in precompute-assignments request, for exposure logging via in-memory storage and enrichment from the SDK core.
This establishes the foundation for feature flag targeting by allowing developers to set evaluation context that gets automatically enhanced with device, app, and user data from the Datadog SDK core.
Context, Targeting Data, Attributes; these are all words that mean the set of properties the flag evaluation system can use to determine the effective value of a flag for a particular user or subject (or context). The
targetingKeyis the pillar of the evaluation context, impacting both targeting rules and the selection of traffic for Experiments (such as an A/B test). An inconsistenttargetingKeyfor a user leads to inconsistent bucketing.Motivation
The feature flags implementation needs context management to:
How to navigate this PR
How to Navigate This PR
🎯 Start Here - Core API Changes
FlagsProvider.kt- See the newsetContext(targetingKey, attributes)APIFlagsConfiguration.kt- NewuseDatadogContextForEvaluationsoptionEvaluationContext.kt- New Kotlin DSL for context creation🏗️ Architecture Overview
FlagsContextManager.kt- Central context management with SDK core integrationDefaultFlagsRepository.kt- Context storage and network coordinationDefaultFlagsNetworkManager.kt- Context inclusion in network requests🧪 Understanding Through Tests
EvaluationContextTest.kt- See the new DSL syntax examplesFlagsContextIntegrationTest.kt- End-to-end context flowFlagsContextManagerTest.kt- SDK core integration examples💡 Understanding the Flow
DatadogFlagsProvider.setContext()(line 37)DefaultFlagsRepository.updateContext()(line 53)FlagsContextManager.enhanceWithCoreContext()(line 100+)DefaultFlagsNetworkManager.buildRequestBody()(line 178+)Usage Examples
Basic Context Setting
SDK Core Integration