Skip to content
3 changes: 3 additions & 0 deletions features/dd-sdk-android-flags/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class com.datadog.android.flags._FlagsInternalProxy
fun isEvaluationTelemetryOptOutParameterEnabled(): Boolean
fun getFlagAssignmentsSnapshot(): Map<String, com.datadog.android.flags.model.UnparsedFlag>
fun trackFlagSnapshotEvaluation(String, com.datadog.android.flags.model.UnparsedFlag, com.datadog.android.flags.model.EvaluationContext)
fun trackFlagExposure(String, com.datadog.android.flags.model.UnparsedFlag, com.datadog.android.flags.model.EvaluationContext)
fun trackFlagEvaluationEvent(String, com.datadog.android.flags.model.UnparsedFlag?, com.datadog.android.flags.model.EvaluationContext, String?)
fun trackFlagRumEvaluation(String, String, com.datadog.android.flags.model.EvaluationContext)
enum com.datadog.android.flags.model.ErrorCode
- PROVIDER_NOT_READY
- FLAG_NOT_FOUND
Expand Down
3 changes: 3 additions & 0 deletions features/dd-sdk-android-flags/api/dd-sdk-android-flags.api
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public final class com/datadog/android/flags/_FlagsInternalProxy {
public final fun getFlagAssignmentsSnapshot ()Ljava/util/Map;
public final fun isEvaluationTelemetryOptOutParameterEnabled ()Z
public final fun setEvaluationContext (Lcom/datadog/android/flags/model/EvaluationContext;Lcom/datadog/android/flags/EvaluationContextCallback;Z)V
public final fun trackFlagEvaluationEvent (Ljava/lang/String;Lcom/datadog/android/flags/model/UnparsedFlag;Lcom/datadog/android/flags/model/EvaluationContext;Ljava/lang/String;)V
public final fun trackFlagExposure (Ljava/lang/String;Lcom/datadog/android/flags/model/UnparsedFlag;Lcom/datadog/android/flags/model/EvaluationContext;)V
public final fun trackFlagRumEvaluation (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/flags/model/EvaluationContext;)V
public final fun trackFlagSnapshotEvaluation (Ljava/lang/String;Lcom/datadog/android/flags/model/UnparsedFlag;Lcom/datadog/android/flags/model/EvaluationContext;)V
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,50 @@ class _FlagsInternalProxy(private val client: FlagsClient) {
emptyMap()
}

/**
* Records exposure, evaluation, and RUM tracking according to the enabled native features.
* Use the separate operations below for independently selected tracking hooks.
*/
fun trackFlagSnapshotEvaluation(flagKey: String, flag: UnparsedFlag, context: EvaluationContext) {
if (client is DatadogFlagsClient) {
client.trackFlagSnapshotEvaluation(flagKey, flag, context)
}
}

/**
* Records only an exposure, using the flag and context captured by the caller, without fetching configuration.
* Evaluation telemetry opt-out intentionally does not suppress exposures.
*/
fun trackFlagExposure(flagKey: String, flag: UnparsedFlag, context: EvaluationContext) {
if (client is DatadogFlagsClient) {
client.trackFlagExposure(flagKey, flag, context)
}
}

/**
* Records only an evaluation event, without fetching configuration or reporting to RUM.
* Respects the client's evaluation telemetry opt-out settings.
* A missing flag represents a runtime default. Unknown OpenFeature error codes are reported as `GENERAL`.
* The sanitized code is sent as the event's error message, matching the iOS bridge.
*/
fun trackFlagEvaluationEvent(
flagKey: String,
flag: UnparsedFlag?,
context: EvaluationContext,
errorCode: String?
) {
if (client is DatadogFlagsClient) {
client.trackFlagEvaluationEvent(flagKey, flag, context, errorCode)
}
}

/**
* Records only a RUM flag annotation, respecting the client's evaluation telemetry opt-out settings.
* Does not fetch configuration, record an exposure, or record an evaluation event.
*/
fun trackFlagRumEvaluation(flagKey: String, variantKey: String, context: EvaluationContext) {
if (client is DatadogFlagsClient) {
client.trackFlagRumEvaluation(flagKey, variantKey, context)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ internal class DatadogFlagsClient(
evaluationTelemetryOptOut ||
(subjectOptOut[requestedSubject] == true) || (subjectOptOut[subject] == true)

private fun isEvaluationTelemetryOptedOut(context: EvaluationContext): Boolean =
evaluationTelemetryOptOutParameterEnabled &&
synchronized(telemetryLock) { isSubjectOptedOut(context.targetingKey) }

/**
* Resolves a boolean flag value.
*
Expand Down Expand Up @@ -559,12 +563,7 @@ internal class DatadogFlagsClient(
context: EvaluationContext,
telemetryOptedOut: Boolean
) {
// Exposure logging (only when doLog=true)
if (flag.doLog) {
if (flagsConfiguration.trackExposures) {
writeExposureEvent(flagKey, flag, context)
}
}
trackFlagExposure(flagKey, flag, context)

if (telemetryOptedOut) {
return
Expand All @@ -575,7 +574,6 @@ internal class DatadogFlagsClient(
logEvaluation(flagKey, flag.variationKey)
}

// Evaluation logging for all evaluations (when enabled)
if (flagsConfiguration.trackEvaluations) {
writeEvaluationEvent(
flagKey = flagKey,
Expand Down Expand Up @@ -622,9 +620,61 @@ internal class DatadogFlagsClient(
* Supposed to be used by internal Datadog packages to track flag evaluations from an exact flags state snapshot.
*/
internal fun trackFlagSnapshotEvaluation(flagKey: String, flag: UnparsedFlag, context: EvaluationContext) {
val optedOut = synchronized(telemetryLock) { isSubjectOptedOut(context.targetingKey) }
trackResolution(flagKey, flag, context, optedOut)
trackResolution(flagKey, flag, context, isEvaluationTelemetryOptedOut(context))
}

internal fun trackFlagExposure(flagKey: String, flag: UnparsedFlag, context: EvaluationContext) {
if (flagsConfiguration.trackExposures && flag.doLog) {
writeExposureEvent(flagKey, flag, context)
}
}

internal fun trackFlagEvaluationEvent(
flagKey: String,
flag: UnparsedFlag?,
context: EvaluationContext,
errorCode: String?
) {
if (!flagsConfiguration.trackEvaluations || isEvaluationTelemetryOptedOut(context)) {
return
}

// Hook errors can contain arbitrary customer data. Only forward standard OpenFeature codes.
val sanitizedErrorCode = when (errorCode) {
null -> null
in ALLOWED_EVALUATION_ERROR_CODES -> errorCode
else -> "GENERAL"
}
writeEvaluationEvent(
flagKey = flagKey,
context = context,
variantKey = flag?.variationKey.takeIf { sanitizedErrorCode == null },
allocationKey = flag?.allocationKey.takeIf { sanitizedErrorCode == null },
errorCode = sanitizedErrorCode,
errorMessage = sanitizedErrorCode
)
}

internal fun trackFlagRumEvaluation(flagKey: String, variantKey: String, context: EvaluationContext) {
if (flagsConfiguration.rumIntegrationEnabled && variantKey.isNotBlank() &&
!isEvaluationTelemetryOptedOut(context)
) {
logEvaluation(flagKey, variantKey)
}
}

// endregion

private companion object {
val ALLOWED_EVALUATION_ERROR_CODES = setOf(
"PROVIDER_NOT_READY",
"PROVIDER_FATAL",
"FLAG_NOT_FOUND",
"PARSE_ERROR",
"TYPE_MISMATCH",
"TARGETING_KEY_MISSING",
"INVALID_CONTEXT",
"GENERAL"
)
}
}
Loading
Loading