Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d600473
Support obfuscated feature flag assignment keys
leoromanovsky Oct 1, 2026
185a1fa
fix(flags): keep capability negotiation independent of versions
leoromanovsky Oct 2, 2026
b3779f8
test(flags): keep obfuscation coverage in JVM suite
leoromanovsky Oct 6, 2026
39ae47b
Merge origin/develop into flag-key-obfuscation
leoromanovsky Oct 6, 2026
c63e2d6
fix(flags): gate encoded cache reads until source is known
leoromanovsky Oct 6, 2026
16292d0
Use the feature flags capabilities header
leoromanovsky Oct 6, 2026
751cb76
fix(flags): cache lookup hashes and strengthen obfuscation regressions
leoromanovsky Oct 6, 2026
1c1f39f
Merge develop and preserve obfuscation with first-flags callbacks
leoromanovsky Oct 7, 2026
d8a2840
FFLSDK-254: Merge develop into flag-key-obfuscation
leoromanovsky Oct 7, 2026
65fc35c
FFLSDK-254: Mark obfuscation test setup, actions, and assertions
leoromanovsky Oct 8, 2026
3c503fc
FFLSDK-254: Use Mockito annotations in obfuscation tests
leoromanovsky Oct 8, 2026
26d87d6
FFLSDK-254: Generate incidental obfuscation test fixtures
leoromanovsky Oct 8, 2026
ad7aa8d
FFLSDK-254: Limit obfuscation suppressions to specific calls
leoromanovsky Oct 8, 2026
1d236bc
FFLSDK-254: Declare obfuscation parser exceptions
leoromanovsky Oct 8, 2026
71cc4df
FFLSDK-254: Generate assignment and cache JSON models
leoromanovsky Oct 8, 2026
935ada0
FFLSDK-261: Add context to generated assignments test
leoromanovsky Oct 8, 2026
586f86a
FFLSDK-261: Explain obfuscation copy visibility
leoromanovsky Oct 8, 2026
36c7d5a
FFLSDK-261: Merge latest develop
leoromanovsky Oct 8, 2026
723409b
FFLSDK-261: Apply documented Kotlin conventions
leoromanovsky Oct 8, 2026
f4a6d1f
FFLSDK-261: Tighten obfuscation test assertions
leoromanovsky Oct 8, 2026
27ba428
FFLSDK-261: Keep evaluation tests independent of HTTP
leoromanovsky Oct 8, 2026
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 @@ -38,6 +38,7 @@ abstract class GenerateJsonSchemaTask : DefaultTask() {
init {
group = "datadog"
description = "Read source JSON schema files and generate the relevant Kotlin data classes"
internalModels.convention(false)
}

// region Input/Output
Expand Down Expand Up @@ -80,6 +81,10 @@ abstract class GenerateJsonSchemaTask : DefaultTask() {
@get:Input
abstract val targetPackageName: Property<String>

/** Keep generated models out of the public SDK API when requested. */
@get:Input
abstract val internalModels: Property<Boolean>

/**
* The list of schema files to be ignored.
*/
Expand Down Expand Up @@ -124,7 +129,7 @@ abstract class GenerateJsonSchemaTask : DefaultTask() {
logger.info("Found ${files.size} files in input dir: $inputDir")

val reader = JsonSchemaReader(inputNameMapping.get(), logger)
val generator = FileGenerator(outputDir, targetPackageName.get(), logger)
val generator = FileGenerator(outputDir, targetPackageName.get(), logger, internalModels.get())
files.forEach {
val type = reader.readSchema(it)
generator.generate(type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ package com.datadog.gradle.plugin.jsonschema.generator
import com.datadog.gradle.plugin.jsonschema.TypeDefinition
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.TypeSpec
import org.gradle.api.logging.Logger
import java.io.File

class FileGenerator(
private val outputDir: File,
private val packageName: String,
private val logger: Logger
private val logger: Logger,
private val internalModels: Boolean = false
) {

private val knownTypes: MutableSet<KotlinTypeWrapper> = mutableSetOf()
Expand Down Expand Up @@ -75,6 +77,9 @@ class FileGenerator(
).apply { written = true }
)
val topLevelTypeBuilder = generateTypeSpec(definition, rootTypeName)
if (internalModels) {
topLevelTypeBuilder.addModifiers(KModifier.INTERNAL)
}

while (knownTypes.any(isUnwrittenClass)) {
val nestedClasses = knownTypes.filter(isUnwrittenClass).toSet()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import com.datadog.gradle.plugin.jsonschema.UserMerged
import com.datadog.gradle.plugin.jsonschema.Version
import com.datadog.gradle.plugin.jsonschema.Video
import com.datadog.gradle.plugin.jsonschema.WeirdCombo
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
Expand All @@ -50,6 +52,19 @@ import java.nio.file.Paths

class FileGeneratorTest {

@Test
fun `M generate internal model W generate() { internal package }`(@TempDir tempDir: File) {
// Given
val testedGenerator = FileGenerator(tempDir, "com.example.internal.model", NoOpLogger(), internalModels = true)

// When
testedGenerator.generate(Person)

// Then
val generated = File(tempDir, "com/example/internal/model/Person.kt").readText()
assertTrue(generated.contains("internal data class Person("))
}

@ParameterizedTest
@MethodSource("data")
fun `generates a Poko file`(
Expand Down
7 changes: 7 additions & 0 deletions detekt_custom_safe_calls_third_party.yml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ datadog:
- "kotlin.collections.Iterable.toJsonArray()"
- "kotlin.collections.Iterable.toList()"
- "kotlin.collections.Iterable.toMap(kotlin.collections.MutableMap)"
- "kotlin.collections.Iterator.forEach(kotlin.Function1)"
- "kotlin.collections.LinkedHashMap()"
- "kotlin.collections.LinkedHashMap(kotlin.collections.MutableMap?)"
- "kotlin.collections.List.all(kotlin.Function1)"
Expand Down Expand Up @@ -693,6 +694,7 @@ datadog:
- "kotlin.collections.MutableMap.iterator()"
- "kotlin.collections.MutableMap.orEmpty()"
- "kotlin.collections.MutableMap.map(kotlin.Function1)"
- "kotlin.collections.MutableMap.mapNotNull(kotlin.Function1)"
- "kotlin.collections.MutableMap.mapValues(kotlin.Function1)"
- "kotlin.collections.MutableMap.put(java.lang.Class, kotlin.String?)"
- "kotlin.collections.MutableMap.put(kotlin.Any, kotlin.Any?)"
Expand Down Expand Up @@ -741,6 +743,7 @@ datadog:
- "kotlin.collections.MutableSet.forEach(kotlin.Function1)"
- "kotlin.collections.MutableSet.elementAtOrNull(kotlin.Int)"
- "kotlin.collections.MutableSet.indexOf(kotlin.String)"
- "kotlin.collections.MutableSet.iterator()"
- "kotlin.collections.MutableSet.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
- "kotlin.collections.MutableSet.map(kotlin.Function1)"
- "kotlin.collections.MutableSet.remove(android.app.Activity?)"
Expand Down Expand Up @@ -871,7 +874,9 @@ datadog:
- "kotlin.ByteArray.constructor(kotlin.Int)"
- "kotlin.ByteArray.copyTo(kotlin.Int, kotlin.ByteArray, kotlin.Int, kotlin.Int, com.datadog.android.api.InternalLogger)"
- "kotlin.Char.digitToIntOrNull()"
- "kotlin.Char.isHighSurrogate()"
- "kotlin.Char.isLowerCase()"
- "kotlin.Char.isLowSurrogate()"
- "kotlin.Char.titlecase(java.util.Locale)"
- "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)"
- "kotlin.Double.coerceAtMost(kotlin.Double)"
Expand Down Expand Up @@ -988,6 +993,7 @@ datadog:
- "kotlin.String.count(kotlin.Function1)"
- "kotlin.String.endsWith(kotlin.Char, kotlin.Boolean)"
- "kotlin.String.endsWith(kotlin.String, kotlin.Boolean)"
- "kotlin.String.equals(kotlin.String?, kotlin.Boolean)"
- "kotlin.String.filter(kotlin.Function1)"
- "kotlin.String.format(java.util.Locale?, kotlin.Array)"
- "kotlin.String.format(java.util.Locale?, kotlin.String, kotlin.Array)"
Expand Down Expand Up @@ -1161,6 +1167,7 @@ datadog:
- "org.json.JSONArray.length()"
- "org.json.JSONArray.put(kotlin.Any?)"
- "org.json.JSONArray.toJsonArray()"
- "org.json.JSONException.constructor(kotlin.String?)"
- "org.json.JSONObject.constructor()"
- "org.json.JSONObject.has(kotlin.String?)"
- "org.json.JSONObject.keys()"
Expand Down
3 changes: 3 additions & 0 deletions detekt_custom_unsafe_calls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ datadog:
- "androidx.collection.LruCache.get(java.io.File):java.lang.NullPointerException"
- "androidx.collection.LruCache.get(kotlin.String):java.lang.NullPointerException"
- "androidx.collection.LruCache.put(java.io.File, kotlin.Unit):java.lang.NullPointerException"
- "androidx.collection.LruCache.put(kotlin.String, kotlin.String):java.lang.NullPointerException"
- "androidx.collection.LruCache.remove(java.io.File):java.lang.NullPointerException"
- "androidx.metrics.performance.JankStats.createAndTrack(android.view.Window, androidx.metrics.performance.JankStats.OnFrameListener):java.lang.IllegalStateException"
- "androidx.navigation.Navigation.findNavController(android.app.Activity, kotlin.Int):java.lang.IllegalStateException"
Expand Down Expand Up @@ -191,6 +192,7 @@ datadog:
- "java.util.zip.Deflater.deflate(kotlin.ByteArray, kotlin.Int, kotlin.Int, kotlin.Int):java.lang.IllegalArgumentException"
# endregion
# region Kotlin primitives
- "kotlin.ByteArray.constructor(kotlin.Int, kotlin.Function1):java.lang.NegativeArraySizeException"
- "kotlin.ByteArray.copyOf(kotlin.Int):java.lang.NegativeArraySizeException"
- "kotlin.ByteArray.copyOfRange(kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException,java.lang.IllegalArgumentException"
- "kotlin.ByteArray.get(kotlin.Int):java.lang.IndexOutOfBoundsException"
Expand All @@ -205,6 +207,7 @@ datadog:
- "kotlin.String.substring(kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException"
- "kotlin.String.takeLast(kotlin.Int):java.lang.IllegalArgumentException"
- "kotlin.String.takeLast(kotlin.Int):kotlin.IllegalArgumentException"
- "kotlin.String.toInt(kotlin.Int):java.lang.NumberFormatException,java.lang.IllegalArgumentException"
- "kotlin.String.toLong():java.lang.NumberFormatException"
- "kotlin.collections.MutableSet.first():java.util.NoSuchElementException"
- "kotlin.String.format(kotlin.Array):java.util.IllegalFormatException"
Expand Down
6 changes: 6 additions & 0 deletions features/dd-sdk-android-flags/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ createJsonModelsGenerationTask("generateFlagsModelsFromJson") {
targetPackageName = "com.datadog.android.flags.model"
}

createJsonModelsGenerationTask("generateFlagsInternalModelsFromJson") {
inputDirPath = "src/main/json/internal"
targetPackageName = "com.datadog.android.flags.internal.model.generated"
internalModels.set(true)
}

// TODO RUM-18189 Support new AGP DSL
@Suppress("DEPRECATION")
android {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Assignment",
"description": "An assignment after conversion of its value to the SDK string representation.",
"type": "object",
"properties": {
"variationType": { "type": "string" },
"variationValue": { "type": "string" },
"doLog": { "type": "boolean" },
"allocationKey": { "type": "string" },
"variationKey": { "type": "string" },
"extraLogging": { "type": "object", "additionalProperties": true },
"reason": { "type": "string" },
"serialId": { "type": "integer" }
},
"required": ["variationType", "variationValue", "doLog", "allocationKey", "variationKey", "extraLogging", "reason"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "AssignmentsResponse",
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"attributes": {
"type": "object",
"properties": {
"flags": { "type": "object", "additionalProperties": true }
},
"required": ["flags"],
"additionalProperties": true
}
},
"required": ["attributes"]
}
},
"required": ["data"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "FlagsState",
"type": "object",
"properties": {
"evaluationContext": {
"type": "object",
"properties": {
"targetingKey": { "type": "string" },
"attributes": { "type": "object", "additionalProperties": true }
},
"required": ["targetingKey"]
},
"flags": { "type": "object", "additionalProperties": true },
"encodedFlags": { "type": "object", "additionalProperties": true },
"lastUpdateTimestamp": { "type": "integer" },
"obfuscated": { "type": "boolean" },
"obfuscation": { "$ref": "obfuscation.json" }
},
"required": ["evaluationContext", "lastUpdateTimestamp"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Obfuscation",
"type": "object",
"properties": {
"scheme": { "type": "string" },
"salt": { "type": "string" }
},
"required": ["scheme", "salt"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ interface FlagsClient {
/**
* Invokes [listener] once with the keys from the first installed cached or downloaded flags,
* including an empty configuration. Missing or invalid cache does not trigger this callback.
* Obfuscated configurations return an empty key list because the original keys are not available.
* Each registration receives a separate event with a copy of the retained first keys,
* even after subsequent flag updates.
* Replaying an available result reads retained memory and does not perform I/O.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.datadog.android.core.internal.utils.executeSafe
import com.datadog.android.flags.EvaluationContextCallback
import com.datadog.android.flags.FlagsInitializationTimeoutException
import com.datadog.android.flags.internal.FlagsStateManager
import com.datadog.android.flags.internal.model.PrecomputedFlag
import com.datadog.android.flags.internal.model.PrecomputedAssignments
import com.datadog.android.flags.internal.net.NetworkRequestFailedException
import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader
import com.datadog.android.flags.internal.repository.FlagsRepository
Expand Down Expand Up @@ -98,7 +98,7 @@ internal class EvaluationsManager(
* Processes a new evaluation context by fetching flags and storing atomically.
*
* This method asynchronously fetches precomputed flag evaluations for the given context
* and atomically updates both the context and flag data in the repository. A failed or unreadable
* and atomically updates the context, flag data, and encoding descriptor in the repository. A failed or unreadable
* response keeps the last known good flags and their context.
*
* The operation is performed on the configured executor service and will not block the
Expand All @@ -117,6 +117,8 @@ internal class EvaluationsManager(

sdkCore.getFeature(Feature.FLAGS_FEATURE_NAME)
?.withContext(withFeatureContexts = setOf(Feature.RUM_FEATURE_NAME)) { datadogContext ->
val supportsObfuscation = datadogContext.source == "android"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is needed? the same SDK working in React Native SDK setup changes behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaScript consumes the snapshot through the bridge. It cannot decode obfuscated keys yet. A native SDK upgrade alone does not make that JavaScript reader compatible.

  • How do you think we should handle adding support for this given that the React Native changes have to be layer after this release?

flagsRepository.setObfuscationSupported(supportsObfuscation)
executorService.executeSafe(
operationName = FETCH_AND_STORE_OPERATION_NAME,
internalLogger = internalLogger
Expand All @@ -132,9 +134,10 @@ internal class EvaluationsManager(
hadFlags && flagsRepository.getEvaluationContext() == context
)
val response = assignmentsReader.readPrecomputedFlags(context, datadogContext)
val flagsMap = response?.let { precomputeMapper.map(it) }
if (flagsMap != null) {
installFlags(context, flagsMap, initializationCompletion, callback)
val assignments = response?.let { precomputeMapper.map(it) }
?.takeIf { it.obfuscation == null || supportsObfuscation }
if (assignments != null) {
Comment thread
leoromanovsky marked this conversation as resolved.
installFlags(context, assignments, initializationCompletion, callback)
} else {
val message = if (response == null) NETWORK_REQUEST_FAILED_MESSAGE else INVALID_RESPONSE_MESSAGE
internalLogger.log(
Expand Down Expand Up @@ -164,11 +167,11 @@ internal class EvaluationsManager(

private fun installFlags(
context: EvaluationContext,
flagsMap: Map<String, PrecomputedFlag>,
assignments: PrecomputedAssignments,
initializationCompletion: InitializationCompletion?,
callback: EvaluationContextCallback?
) {
flagsRepository.setFlagsAndContext(context, flagsMap) {
flagsRepository.setFlagsAndContext(context, assignments.flags, assignments.obfuscation) {
val completionCallback = synchronized(initializationTerminalLock) {
val result = initializationCompletion?.take()?.callback
?: if (initializationCompletion == null) callback else null
Expand All @@ -180,7 +183,7 @@ internal class EvaluationsManager(
internalLogger.log(
InternalLogger.Level.DEBUG,
InternalLogger.Target.MAINTAINER,
{ "Successfully processed context ${context.targetingKey} with ${flagsMap.size} flags" }
{ "Successfully processed context ${context.targetingKey} with ${assignments.flags.size} flags" }
)
}

Expand Down
Loading
Loading