Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
758845b
feat(rum): let sampling rates be set remotely instead of only at init
Fiona2016 Aug 15, 2026
488e9f1
feat(rum): ask for the sampling settings again when the app returns
Fiona2016 Aug 16, 2026
915f0ef
feat(rum): report which settings version the app is running
Fiona2016 Aug 16, 2026
c65c532
fix(rum): only refresh on foreground when the server allows it
Fiona2016 Aug 17, 2026
e63ad9d
feat(rum): let the host application force a session to be collected
Fiona2016 Aug 19, 2026
307c427
feat(rum): deliver the console's custom values to the host application
Fiona2016 Aug 19, 2026
dc1ad6b
refactor(rum): rename the remote sampling channel to remote configura…
Fiona2016 Aug 24, 2026
1eee2d5
feat(rum): fetch remote configuration per session instead of polling
Fiona2016 Aug 24, 2026
5582bd4
feat(rum): revalidate the stored configuration instead of refetching it
Fiona2016 Aug 24, 2026
b620775
feat(rum): report the configuration a session was drawn under on its …
Fiona2016 Aug 24, 2026
6a04de3
chore(rum): record rcVersion in the API surface
Fiona2016 Aug 24, 2026
f21498c
refactor(rum): drop remote delivery of the Session Replay sample rate
Fiona2016 Aug 24, 2026
3f6e630
refactor(rum): return the console's custom values decoded
Fiona2016 Aug 24, 2026
a729603
refactor(rum): drop what the narrowed scope left behind
Fiona2016 Aug 25, 2026
beba6f7
fix(rum): refuse configuration responses this SDK cannot vouch for
Fiona2016 Aug 27, 2026
b3afd61
feat(rum): let the application have the last word on the session draw
Fiona2016 Aug 27, 2026
a0a744e
refactor(rum): drop the draw record nothing reads
Fiona2016 Aug 27, 2026
adce02c
fix(rum): read a configuration that carries no schema stamp
Fiona2016 Aug 27, 2026
2665e54
refactor(rum): keep only the part of the draw record anything reads
Fiona2016 Aug 27, 2026
dc0f36c
fix(rum): sweep the stored configuration of versions the device no lo…
Fiona2016 Sep 2, 2026
6be288e
style(rum): drop a blank line ktlint counts twice
Fiona2016 Sep 3, 2026
9a890fb
style(rum): apply ktlint formatting to the files this branch changed
Fiona2016 Sep 3, 2026
eb9ec24
fix(rum): carry a new session's draw into the view that survives its …
Fiona2016 Sep 4, 2026
b19727a
fix(rum): make a forced session mean what its documentation says
Fiona2016 Sep 4, 2026
bde7544
fix(rum): keep the refresh rhythm, and refuse a body that is not a co…
Fiona2016 Sep 4, 2026
6c935dd
docs: add the 0.7.0 changelog entry for remote configuration
Fiona2016 Sep 4, 2026
db36f06
docs: place the 0.7.0 entry above the released 0.6.0 one
Fiona2016 Sep 4, 2026
2d27107
feat(rum): apply a rate crossing zero to the running session
Fiona2016 Sep 4, 2026
3e2dbbe
fix(rum): keep remote sampling snapshots consistent
Fiona2016 Sep 5, 2026
b121dd2
Merge publish into remote sampling configuration
Fiona2016 Sep 5, 2026
b132472
fix(rum): handle remote configuration lifecycle and sampling edges
Fiona2016 Sep 7, 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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 0.7.0 / Unreleased

* [FEATURE] RUM: take the session sample rate from the application's settings in the Flashcat console instead of only from the value passed to `RumConfiguration.Builder.setSessionSampleRate()`, so it can be changed without shipping a new release of the app. Off by default — enable it with `RumConfiguration.Builder.setRemoteConfigurationEnabled(true)`. Left off, the SDK makes no extra request, opens no extra storage, starts no extra thread, and behaves exactly as it did before. A change applies to sessions started after it arrives, so a session already under way is normally never redrawn. Two changes are exceptions and end the running session so the next one starts under the new rate: one the console marks for immediate activation, and a rate crossing zero in either direction — switching collection off is an emergency stop, and switching it back on has nothing to preserve, since while the rate was zero no session was being collected and none was drawn against a real rate. The values passed to init keep applying until the first settings arrive and whenever the endpoint cannot be reached — a failed, timed-out or unreadable response never moves a rate. Events report the rate the session was actually drawn under, together with the settings version it came from, so server-side extrapolation and audits line up with the draw.

* [FEATURE] RUM: add `RumConfiguration.Builder.setBeforeSampling()`, a callback consulted at each session draw with the rate that would apply and the console's custom values. Return a rate to override it, or `null` to leave it alone. It is the last step of the draw, after the console's rate, so an allow-list can keep collecting a user the console's rate would drop. A callback that throws, or returns a rate outside 0..100, is ignored and the incoming rate applies.

* [FEATURE] RUM: add `RumMonitor.setForcedSession()` and `RumMonitor.getRemoteConfig()`. `setForcedSession()` collects the user's sessions with Session Replay regardless of the configured rates; the state lasts for the process lifetime and survives `stopSession()`. Events from a forced session report a sample rate of 100 and no settings version, because the session was kept whatever the rates said. `getRemoteConfig()` returns the application-defined values published in the console, delivered verbatim and never interpreted by the SDK. **Breaking:** `RumMonitor` is a public interface and gained two methods with no default implementation, so any class that implements it directly — a test double or a wrapper, most likely — must add them to compile. Code that only calls `GlobalRumMonitor.get()` is unaffected.

* [IMPROVEMENT] RUM: `ViewEvent.Configuration` carries a new optional `rcVersion` field, so its constructor and `copy()` take one more argument. Kotlin callers that use named or default arguments are unaffected once recompiled; Java callers that construct it directly must pass the extra argument.

---

# 0.6.0 / 2026-08-17

* [CHANGE] Change the default NTP servers from `0.datadog.pool.ntp.org` through `3.datadog.pool.ntp.org` to `ntp.aliyun.com`, `ntp1.aliyun.com`, `time1.cloud.tencent.com` and `cn.pool.ntp.org`. Measured from a mainland-China host, where this SDK is predominantly deployed, the new servers answer at stratum 2 within 12-40 ms, while the previous ones answer at stratum 3 within 41-241 ms and one of the four did not answer at all. The new defaults span two cloud providers and the community pool, so no single operator being unreachable stops the clock from synchronizing. Apps that need other servers can select them with `setNtpHosts`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,14 @@ internal class CoreFeature(
fun createOkHttpCallFactory(block: OkHttpClient.Builder.() -> Unit): Call.Factory {
return object : Call.Factory {
// Create a new client that shares pools with the base client
private val client = lazySharedOkHttpClient.newBuilder()
private val client = callFactory.okhttpClient.newBuilder()
// Feature requests inherit transport settings, not upload encoding or logging.
.apply {
@Suppress("UnsafeThirdPartyFunctionCall") // Returns the builder's mutable list.
interceptors().clear()
@Suppress("UnsafeThirdPartyFunctionCall") // Returns the builder's mutable list.
networkInterceptors().clear()
}
.apply(block)
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import okhttp3.CipherSuite
import okhttp3.ConnectionSpec
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.TlsVersion
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
Expand Down Expand Up @@ -1670,4 +1672,66 @@ internal class CoreFeatureTest {
return listOf(appContext)
}
}

@Test
fun `M remote factory preserves configured proxy W configuration changes`() {
val proxy = java.net.Proxy(java.net.Proxy.Type.HTTP, java.net.InetSocketAddress("127.0.0.1", 18888))
val proxyAuth: Authenticator = mock()
testedFeature.initialize(
appContext.mockInstance,
fakeSdkInstanceId,
fakeConfig.copy(coreConfig = fakeConfig.coreConfig.copy(proxy = proxy, proxyAuth = proxyAuth)),
fakeConsent
)
assertThat(testedFeature.callFactory.okhttpClient.proxy).isSameAs(proxy)
val call = testedFeature.createOkHttpCallFactory {}.newCall(
Request.Builder().url("https://example.com").build()
)
val field = call.javaClass.getDeclaredField("client").apply { isAccessible = true }
val client = field.get(call) as okhttp3.OkHttpClient
assertThat(client.proxy).isSameAs(proxy)
assertThat(client.proxyAuthenticator).isSameAs(proxyAuth)
}

@Test
fun `M remote factory preserves explicitly allowed cleartext W configuration changes`() {
testedFeature.initialize(
appContext.mockInstance,
fakeSdkInstanceId,
fakeConfig.copy(coreConfig = fakeConfig.coreConfig.copy(needsClearTextHttp = true)),
fakeConsent
)
assertThat(testedFeature.callFactory.okhttpClient.connectionSpecs).contains(ConnectionSpec.CLEARTEXT)
val call = testedFeature.createOkHttpCallFactory {
}.newCall(Request.Builder().url("http://127.0.0.1:18889/config").build())
val field = call.javaClass.getDeclaredField("client").apply { isAccessible = true }
val client = field.get(call) as okhttp3.OkHttpClient
assertThat(client.connectionSpecs).contains(ConnectionSpec.CLEARTEXT)
}

@Test
fun `M preserve feature POST bodies W creating a configured client`() {
testedFeature.initialize(appContext.mockInstance, fakeSdkInstanceId, fakeConfig, fakeConsent)
val body = "plain feature payload".toByteArray()
val factory = testedFeature.createOkHttpCallFactory {
addInterceptor { chain ->
val request = chain.request()
val buffer = okio.Buffer()
request.body!!.writeTo(buffer)
assertThat(buffer.readByteArray()).isEqualTo(body)
assertThat(request.header("Content-Encoding")).isNull()
okhttp3.Response.Builder().request(request).protocol(Protocol.HTTP_1_1)
.code(200).message("OK").body("".toResponseBody()).build()
}
}
val call = factory.newCall(
Request.Builder().url("https://example.com/flags")
.post(body.toRequestBody()).build()
)
val field = call.javaClass.getDeclaredField("client").apply { isAccessible = true }
val client = field.get(call) as okhttp3.OkHttpClient
assertThat(client.interceptors).hasSize(1)
assertThat(client.networkInterceptors).isEmpty()
assertThat(call.execute().code).isEqualTo(200)
}
}
10 changes: 9 additions & 1 deletion features/dd-sdk-android-rum/api/apiSurface
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
data class com.datadog.android.rum.BeforeSamplingContext
constructor(Float, Map<String, Any?>?)
interface com.datadog.android.rum.BeforeSamplingCallback
fun sampleRate(BeforeSamplingContext): Float?
fun <T: java.io.Closeable, R> T.useMonitored(com.datadog.android.api.SdkCore = Datadog.getInstance(), (T) -> R): R
annotation com.datadog.android.rum.ExperimentalRumApi
object com.datadog.android.rum.GlobalRumMonitor
Expand Down Expand Up @@ -62,6 +66,8 @@ data class com.datadog.android.rum.RumConfiguration
class Builder
constructor(String)
fun setSessionSampleRate(Float): Builder
fun setRemoteConfigurationEnabled(Boolean): Builder
fun setBeforeSampling(BeforeSamplingCallback): Builder
fun collectAccessibility(Boolean): Builder
fun setTelemetrySampleRate(Float): Builder
fun trackUserInteractions(Array<com.datadog.android.rum.tracking.ViewAttributesProvider> = emptyArray(), com.datadog.android.rum.tracking.InteractionPredicate = NoOpInteractionPredicate()): Builder
Expand Down Expand Up @@ -116,6 +122,8 @@ interface com.datadog.android.rum.RumMonitor
fun getAttributes(): Map<String, Any?>
fun clearAttributes()
fun stopSession()
fun setForcedSession()
fun getRemoteConfig(): Map<String, Any?>?
fun addViewLoadingTime(Boolean)
fun addViewAttributes(Map<String, Any?>)
fun removeViewAttributes(Collection<String>)
Expand Down Expand Up @@ -1838,7 +1846,7 @@ data class com.datadog.android.rum.model.ViewEvent
fun fromJson(kotlin.String): DdSession
fun fromJsonObject(com.google.gson.JsonObject): DdSession
data class Configuration
constructor(kotlin.Number, kotlin.Number? = null, kotlin.Number? = null, kotlin.Boolean? = null)
constructor(kotlin.Number, kotlin.Number? = null, kotlin.Number? = null, kotlin.Boolean? = null, kotlin.Long? = null)
fun toJson(): com.google.gson.JsonElement
companion object
fun fromJson(kotlin.String): Configuration
Expand Down
31 changes: 27 additions & 4 deletions features/dd-sdk-android-rum/api/dd-sdk-android-rum.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
public abstract interface class com/datadog/android/rum/BeforeSamplingCallback {
public abstract fun sampleRate (Lcom/datadog/android/rum/BeforeSamplingContext;)Ljava/lang/Float;
}

public final class com/datadog/android/rum/BeforeSamplingContext {
public fun <init> (FLjava/util/Map;)V
public final fun component1 ()F
public final fun component2 ()Ljava/util/Map;
public final fun copy (FLjava/util/Map;)Lcom/datadog/android/rum/BeforeSamplingContext;
public static synthetic fun copy$default (Lcom/datadog/android/rum/BeforeSamplingContext;FLjava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/rum/BeforeSamplingContext;
public fun equals (Ljava/lang/Object;)Z
public final fun getCustom ()Ljava/util/Map;
public final fun getSessionSampleRate ()F
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/datadog/android/rum/CloseableExtKt {
public static final fun useMonitored (Ljava/io/Closeable;Lcom/datadog/android/api/SdkCore;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun useMonitored$default (Ljava/io/Closeable;Lcom/datadog/android/api/SdkCore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
Expand Down Expand Up @@ -103,10 +120,12 @@ public final class com/datadog/android/rum/RumConfiguration$Builder {
public final fun collectAccessibility (Z)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun disableUserInteractionTracking ()Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setActionEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setBeforeSampling (Lcom/datadog/android/rum/BeforeSamplingCallback;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setErrorEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setInitialResourceIdentifier (Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setLastInteractionIdentifier (Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setLongTaskEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setRemoteConfigurationEnabled (Z)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setResourceEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setSessionListener (Lcom/datadog/android/rum/RumSessionListener;)Lcom/datadog/android/rum/RumConfiguration$Builder;
public final fun setSessionSampleRate (F)Lcom/datadog/android/rum/RumConfiguration$Builder;
Expand Down Expand Up @@ -161,10 +180,12 @@ public abstract interface class com/datadog/android/rum/RumMonitor {
public abstract fun getAttributes ()Ljava/util/Map;
public abstract fun getCurrentSessionId (Lkotlin/jvm/functions/Function1;)V
public abstract fun getDebug ()Z
public abstract fun getRemoteConfig ()Ljava/util/Map;
public abstract fun removeAttribute (Ljava/lang/String;)V
public abstract fun removeViewAttributes (Ljava/util/Collection;)V
public abstract fun reportAppFullyDisplayed ()V
public abstract fun setDebug (Z)V
public abstract fun setForcedSession ()V
public abstract fun startAction (Lcom/datadog/android/rum/RumActionType;Ljava/lang/String;Ljava/util/Map;)V
public abstract fun startFeatureOperation (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V
public abstract fun startResource (Ljava/lang/String;Lcom/datadog/android/rum/RumResourceMethod;Ljava/lang/String;Ljava/util/Map;)V
Expand Down Expand Up @@ -4942,18 +4963,20 @@ public final class com/datadog/android/rum/model/ViewEvent$Companion {

public final class com/datadog/android/rum/model/ViewEvent$Configuration {
public static final field Companion Lcom/datadog/android/rum/model/ViewEvent$Configuration$Companion;
public fun <init> (Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;)V
public synthetic fun <init> (Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Long;)V
public synthetic fun <init> (Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Number;
public final fun component2 ()Ljava/lang/Number;
public final fun component3 ()Ljava/lang/Number;
public final fun component4 ()Ljava/lang/Boolean;
public final fun copy (Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;)Lcom/datadog/android/rum/model/ViewEvent$Configuration;
public static synthetic fun copy$default (Lcom/datadog/android/rum/model/ViewEvent$Configuration;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/datadog/android/rum/model/ViewEvent$Configuration;
public final fun component5 ()Ljava/lang/Long;
public final fun copy (Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Long;)Lcom/datadog/android/rum/model/ViewEvent$Configuration;
public static synthetic fun copy$default (Lcom/datadog/android/rum/model/ViewEvent$Configuration;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;Ljava/lang/Long;ILjava/lang/Object;)Lcom/datadog/android/rum/model/ViewEvent$Configuration;
public fun equals (Ljava/lang/Object;)Z
public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/rum/model/ViewEvent$Configuration;
public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/rum/model/ViewEvent$Configuration;
public final fun getProfilingSampleRate ()Ljava/lang/Number;
public final fun getRcVersion ()Ljava/lang/Long;
public final fun getSessionReplaySampleRate ()Ljava/lang/Number;
public final fun getSessionSampleRate ()Ljava/lang/Number;
public final fun getStartSessionReplayRecordingManually ()Ljava/lang/Boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,12 @@
"type": "boolean",
"description": "Whether session replay recording configured to start manually",
"readOnly": true
},
"rc_version": {
"type": "integer",
"description": "FlashCat fork - version of the remote configuration the session was drawn under; 0 when none was ever fetched",
"minimum": 0,
"readOnly": true
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum

/**
* What the SDK is about to draw a new session with, handed to [BeforeSamplingCallback]: the rate
* that would apply (the console's where it published one, the value passed to init where it did
* not) and the console's custom values, decoded.
*
* @param sessionSampleRate the rate, between 0 and 100, that would decide this session.
* @param custom the console's custom values, or null when remote configuration is off or nothing
* is published. Same content as [RumMonitor.getRemoteConfig].
*/
data class BeforeSamplingContext(
val sessionSampleRate: Float,
val custom: Map<String, Any?>?
)

/**
* The application's last word on session sampling, called synchronously each time a new session is
* about to be drawn.
*
* Return a rate to override the one the SDK was going to use — 100 always collects, 0 never does —
* or null to leave it alone. The typical use is an allow-list: keep every session of the handful of
* users you are debugging while the fleet stays at a low rate.
*
* It runs inside session creation, so it must be fast and must not block. A throw, or a rate
* outside 0..100, is ignored and the incoming rate applies: a mistake here must never take a
* customer's collection down with it. A session already under way is never re-decided.
*/
fun interface BeforeSamplingCallback {

/**
* @param context the rate that would apply and the console's custom values.
* @return the rate to draw this session with, or null to keep [BeforeSamplingContext.sessionSampleRate].
*/
fun sampleRate(context: BeforeSamplingContext): Float?
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ object Rum {
sdkCore = sdkCore,
sessionEndedMetricDispatcher = sessionEndedMetricDispatcher,
sampleRate = rumFeature.sampleRate,
remoteConfig = rumFeature.remoteConfigStore,
// FLASHCAT FORK - looked up when it fires rather than captured now: a session start
// simply asks again, and there is nothing to ask with when the app did not opt in.
onSessionDrawn = { rumFeature.remoteConfigController?.onSessionStarted() },
beforeSampling = rumFeature.configuration.beforeSampling,
writer = rumFeature.dataWriter,
handler = Handler(Looper.getMainLooper()),
telemetryEventHandler = TelemetryEventHandler(
Expand Down
Loading
Loading