-
Notifications
You must be signed in to change notification settings - Fork 75
FFL-1460 FlagsClient changes to accommodate Flags RN SDK sync flag evals #3049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
FFL-1460 FlagsClient changes to accommodate Flags RN SDK sync flag evals #3049
Conversation
…nd tracking evaluations
f3efb76 to
ca508fe
Compare
| @Suppress("UNCHECKED_CAST") | ||
| val result: T? = when (targetType) { | ||
| Boolean::class -> variationValue.lowercase(Locale.US).toBooleanStrictOrNull() as? T | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a redundant change, you need to run KtLint version 0.50.0:
ktlint-0.50.0 -F "**/*.kt" "**/*.kts" '!**/build/generated/**' '!**/build/kspCaches/**'
| @Suppress("FunctionName") | ||
| @JvmSynthetic | ||
| fun _getInternal(): _FlagsInternalProxy? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would propose to remove this method: it will still show up in the suggestions dropdown in Kotlin.
One can create an instance of _FlagsInternalProxy by themselves, the constructor is public there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RUM subpackage does the same in RumMonitor. This seemed like an established pattern in the codebase so I mimicked it in the flags package
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a first try to expose private APIs, but this still leaks it to the suggestions dropdown in case of Kotlin (not in Java though), so it is better simply to remove this and go through the manual call of _FlagsInternalProxy constructor.
| private val internalProxy = _FlagsInternalProxy(this) | ||
|
|
||
| override fun _getInternal(): _FlagsInternalProxy = internalProxy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both can be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like moving this class is not necessary? It still stays internal. Can you please rollback this change?
| } | ||
|
|
||
| private fun <T : Any> trackResolution( | ||
| flagKey: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flag Value is a member of the precomputed flag.
| * | ||
| * @return A map of flag key to precomputed flag, or null if no flags are available. | ||
| */ | ||
| internal fun getFlagAssignmentsSnapshot(): Map<String, PrecomputedFlag>? = flagsRepository.getFlagsSnapshot() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the caller get the EvaluationContext from?
What do you think of
interface FlagAssignmentSnapshot {
Map<String, ResolutionDetails> flagAssignments
EvaluationContext: context
}Then, the track method would be
trackResolution(flagKey, flagAssignmentsSnapshot)The trackResolution method would look up the resolution for that flag from the snapshot and the context for tracking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The context is stored on JavaScript side alongside with flags cache, and is being sent from there.
| * @param reason The evaluation reason. | ||
| */ | ||
| @InternalApi | ||
| data class PrecomputedFlag( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which of these fields are needed for including in the Flag Assignments snapshot? I'd prefer to keep an internal model as this specifically represents when we are sending "over the wire" from the edge assignments server to the SDK.
I think ResolutionDetails should have what you need and it's already public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ResolutionDetails doesn't have the doLog property from PrecomputedFlag, which is required in the trackResolution method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Synced on this and agreed to make an external Interface that we implement with our internal data class. This avoids a lot of unnecessary fieldmapping code
features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/_FlagsInternalProxy.kt
Outdated
Show resolved
Hide resolved
...d-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/DatadogFlagsClient.kt
Show resolved
Hide resolved
...dk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/model/PrecomputedFlag.kt
Outdated
Show resolved
Hide resolved
| */ | ||
| @InternalApi | ||
| @Suppress("UndocumentedPublicProperty") | ||
| interface UnparsedFlag { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH I don't really like this naming, open to better name ideas 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think PrecomputedFlag or FlagAssignment would work here. We don't really need to say that it's "unparsed" because the flag assignment itself has been parsed from the server response. It's just that the value has not been typed yet and that's more of an internal concern.
0xnm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some suggestions, nothing blocking, mostly about API/logging. I think it is a final round and we are good to go after.
| fun removeListener(FlagsStateListener) | ||
| class com.datadog.android.flags._FlagsInternalProxy | ||
| constructor(FlagsClient) | ||
| fun getFlagAssignmentsSnapshot(): Map<String, com.datadog.android.flags.model.UnparsedFlag>? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: normally returning nullable collection should be avoided, it is better to return empty collection if there is nothing to return. This way there is no null case to handle at the call site.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the returned collection is null, that means the client initialization has failed at some step. Do you have an alternative to handle the case of the clear failure vs the flags cache is empty so we return an empty collection?
I guess the alternatives would be an pattern Optional (feels better than returning null?) or throwing an exception from this method (personally I don't like this pattern).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for the client initialization we now have FlagsClientState and FlagsClient.state.getCurrentState API, so I wouldn't rely on the null value here to make a state judgement. If RN needs to know the state it is better to use that API.
| fun getFlagAssignmentsSnapshot(): Map<String, UnparsedFlag>? = if (client is DatadogFlagsClient) { | ||
| client.getFlagAssignmentsSnapshot() | ||
| } else { | ||
| null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| null | |
| emptyMap() |
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject - ${exception.message}" | ||
| featureSdkCore.internalLogger.log( | ||
| InternalLogger.Level.WARN, | ||
| InternalLogger.Target.MAINTAINER, | ||
| { "Flag '$flagKey': $errorMessage" } | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is better to attach exception to the log call
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject - ${exception.message}" | |
| featureSdkCore.internalLogger.log( | |
| InternalLogger.Level.WARN, | |
| InternalLogger.Target.MAINTAINER, | |
| { "Flag '$flagKey': $errorMessage" } | |
| ) | |
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject" | |
| featureSdkCore.internalLogger.log( | |
| InternalLogger.Level.WARN, | |
| InternalLogger.Target.MAINTAINER, | |
| { "Flag '$flagKey': $errorMessage" }, | |
| exception | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems there is a request to do another round of review, but I don't see a change related to this comment, there is only a merge of develop since last review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whops forgot to hit the push button
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject - ${exception.message}" | ||
| featureSdkCore.internalLogger.log( | ||
| InternalLogger.Level.WARN, | ||
| InternalLogger.Target.MAINTAINER, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the target be the USER? Since I guess it is about user data parsing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This log is corresponding to the flags data parsing logic in readAndParseAssignment (line 289).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point here is that MAINTAINER target logs are visible only in the SDK debug builds, they won't be shown in the release build. So if user needs to know about this error, it should be USER target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@typotter FYI the target has been changed to USER in other related flag value parsing logic as well. Does that work for you?
| featureSdkCore.internalLogger.log( | ||
| InternalLogger.Level.WARN, | ||
| InternalLogger.Target.MAINTAINER, | ||
| { "Flag '$flagKey': Failed to parse value '${flag.variationValue}' as '${flag.variationType}'" } | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if it is useful, because we already have a log entry above in case of exception, so if exception happens we will have two log entries about the same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This log triggers for all variation types other than JSONObject. This is made this way to avoid swallowing JSONException details and provider them in the corresponding branch.
| InternalLogger.Target.USER, | ||
| { WARN_CONTEXT_NOT_SET } | ||
| ) | ||
| return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| return null | |
| return emptyMap() |
| fun setFlagsAndContext(context: EvaluationContext, flags: Map<String, PrecomputedFlag>) | ||
| fun getPrecomputedFlagWithContext(key: String): Pair<PrecomputedFlag, EvaluationContext>? | ||
| fun hasFlags(): Boolean | ||
| fun getFlagsSnapshot(): Map<String, PrecomputedFlag>? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| fun getFlagsSnapshot(): Map<String, PrecomputedFlag>? | |
| fun getFlagsSnapshot(): Map<String, PrecomputedFlag> |
| verify(mockProcessor).processEvent( | ||
| flagName = eq(fakeFlagKey), | ||
| context = eq(fakeContext), | ||
| data = eq(fakeFlag) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tip: when all matchers are eq, eq usage can be omitted
| verify(mockProcessor).processEvent( | |
| flagName = eq(fakeFlagKey), | |
| context = eq(fakeContext), | |
| data = eq(fakeFlag) | |
| ) | |
| verify(mockProcessor).processEvent( | |
| flagName = fakeFlagKey, | |
| context = fakeContext, | |
| data = fakeFlag | |
| ) |
|
🎯 Code Coverage 🔗 Commit SHA: b566487 | Docs | Datadog PR Page | Was this helpful? Give us feedback! |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #3049 +/- ##
===========================================
+ Coverage 71.24% 71.28% +0.04%
===========================================
Files 866 867 +1
Lines 31730 31773 +43
Branches 5360 5370 +10
===========================================
+ Hits 22604 22648 +44
+ Misses 7602 7597 -5
- Partials 1524 1528 +4
🚀 New features to boost your workflow:
|
0xnm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, added some final remarks.
And I would still strongly recommend to change fun getFlagAssignmentsSnapshot(): Map<String, com.datadog.android.flags.model.UnparsedFlag>? to fun getFlagAssignmentsSnapshot(): Map<String, com.datadog.android.flags.model.UnparsedFlag>, because having a nullable collection is anti-pattern. Once it is out it will be public ABI-breaking change if type should be changed to non-nullable.
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject - ${exception.message}" | ||
| featureSdkCore.internalLogger.log( | ||
| InternalLogger.Level.WARN, | ||
| InternalLogger.Target.MAINTAINER, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point here is that MAINTAINER target logs are visible only in the SDK debug builds, they won't be shown in the release build. So if user needs to know about this error, it should be USER target.
| VariationType.OBJECT.value -> try { | ||
| JSONObject(flag.variationValue) | ||
| } catch (exception: JSONException) { | ||
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject - ${exception.message}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject - ${exception.message}" | |
| val errorMessage = "Failed to parse value '${flag.variationValue}' as JSONObject" |
There is no need to add exception message here, it will already be shown due to passing exception object to the log call.
|
Changed the base branch to Tyler's branch back and forth by a mistake; I will need his callback changes in the React Native PR, not this one. |
| /** | ||
| * Retrieves a snapshot of all flag assignments. | ||
| * | ||
| * Supposed to be used by internal Datadog packages to get flags state snapshot for a given evaluation context. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Supposed to be used by internal Datadog packages to get flags state snapshot for a given evaluation context. | |
| * Explicitly for use by the Datadog React Native SDK to get flags state snapshot for a given evaluation context. |
typotter
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM for FFE SDK
| trackResolution(flagKey, flag, flagValue, context) | ||
| } | ||
|
|
||
| private fun parseFlagValueString(flagKey: String, flag: UnparsedFlag): Any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use the FlagValueConverter here?
What does this PR do?
This PR introduces new exposed "internal" APIs supporting the upcoming Flags SDK for React Native.
Changes:
_FlagsInternalProxyclass forFlagsClientUnparsedFlaginterface to preventPrecomputedFlagfrom being exposed to the public"internal" APIs = exposed APIs that are added purely for consumption within the RN SDK. This implementation follows the example of
_RumInternalProxy.Motivation
The motivation for this work is to allow the upcoming Flags RN SDK to retrieve the complete feature flags state snapshot from the JS side at SDK init time (FFL-1460).
Additional Notes
I've been developing these changes using the example application in the dd-sdk-reactnative repo (by leveraging composite builds)
Sibling PRs in the React Native and iOS repos:
Review checklist (to be filled by reviewers)