Skip to content

firstPartyHosts config is silently ignored on native — JS sends it inside rumConfiguration but Android/iOS read from top level #1154

@AlexanderBartash

Description

@AlexanderBartash

Warning

This was generated with AI, I did try to properly investigate it. Feel free to close if you think this is irrelevant. I found this while investigating #1153 If this bug is valid, it will become a problem after #1153 is fixed.

In SDK v3.x, firstPartyHosts configured via CoreConfigurationrumConfiguration is never received by the native Android or iOS SDK. The JS serialization places it inside the nested rumConfiguration object on the bridge payload, but both native bridges read it from the top level of the configuration map/dictionary — where it doesn't exist in v3.

This means distributed tracing headers (x-datadog-trace-id, x-datadog-parent-id, x-datadog-sampling-priority, traceparent, etc.) are never attached to native network requests, breaking RUM-to-APM trace correlation.

JS-side resource tracking (DdRumResourceTracking) reads firstPartyHosts from rumConfiguration correctly, so trace headers ARE added to JS-intercepted fetch/XMLHttpRequest calls. The bug only affects the native Configuration.Builder.setFirstPartyHostsWithHeaderType() path.


Root cause

JS sideDdSdkNativeConfiguration (the class serialized over the bridge) has no top-level firstPartyHosts. It lives inside rumConfiguration → RumNativeConfiguration:

export class DdSdkNativeConfiguration {
constructor(
readonly additionalConfiguration: object,
readonly clientToken: string,
readonly env: string,
readonly site: string,
readonly service: string | undefined,
readonly verbosity: string | undefined,
readonly trackingConsent: string,
readonly uploadFrequency: string,
readonly batchSize: string,
readonly batchProcessingLevel: BatchProcessingLevel,
readonly proxyConfiguration:
| {
type: string;
address: string;
port: number;
username?: string;
password?: string;
}
| undefined,
readonly attributeEncoders: AttributeEncoder<any>[],
readonly rumConfiguration: RumNativeConfiguration | undefined,

readonly firstPartyHosts: { match: string; propagatorTypes: string[] }[];

Android — reads firstPartyHosts from the root ReadableMap, not from rumMap:

val rumMap = getMap("rumConfiguration")
val logsMap = getMap("logsConfiguration")
val traceMap = getMap("traceConfiguration")
val telemetryMap = getMap("configurationForTelemetry")
val rumConfiguration: RumConfiguration? = rumMap?.let { rm ->
val applicationId = rm.getString("applicationId").orEmpty()
RumConfiguration(
applicationId = applicationId,
trackFrustrations = rm.getBooleanOrNull("trackFrustrations"),
longTaskThresholdMs = rm.getDoubleOrNull("longTaskThresholdMs") ?: 0.0,
sessionSampleRate = rm.getDoubleOrNull("sessionSampleRate"),
resourceTraceSampleRate = rm.getDoubleOrNull("resourceTraceSampleRate"),
vitalsUpdateFrequency = rm.getString("vitalsUpdateFrequency"),
trackBackgroundEvents = rm.getBooleanOrNull("trackBackgroundEvents"),
nativeCrashReportEnabled = rm.getBooleanOrNull("nativeCrashReportEnabled"),
nativeLongTaskThresholdMs = rm.getDoubleOrNull("nativeLongTaskThresholdMs"),
nativeViewTracking = rm.getBooleanOrNull("nativeViewTracking"),
nativeInteractionTracking = rm.getBooleanOrNull("nativeInteractionTracking"),
trackNonFatalAnrs = rm.getBooleanOrNull("trackNonFatalAnrs"),
initialResourceThreshold = rm.getDoubleOrNull("initialResourceThreshold"),
telemetrySampleRate = rm.getDoubleOrNull("telemetrySampleRate"),
customEndpoint = rm.getString("customEndpoint")
)
}
val logsConfiguration: LogsConfiguration? = logsMap?.let { lm ->
LogsConfiguration(
bundleLogsWithRum = lm.getBooleanOrNull("bundleLogsWithRum") ?: true,
bundleLogsWithTraces = lm.getBooleanOrNull("bundleLogsWithTraces") ?: true,
customEndpoint = lm.getString("customEndpoint")
)
}
val traceConfiguration: TraceConfiguration? = traceMap?.let { tm ->
TraceConfiguration(
customEndpoint = tm.getString("customEndpoint")
)
}
val configurationForTelemetry: ConfigurationForTelemetry? =
telemetryMap?.asConfigurationForTelemetry()
return DdSdkConfiguration(
additionalConfiguration = additionalConfiguration?.mapValues { it.value },
clientToken = getString("clientToken").orEmpty(),
env = getString("env").orEmpty(),
site = getString("site"),
service = getString("service"),
verbosity = getString("verbosity"),
trackingConsent = getString("trackingConsent"),
uploadFrequency = getString("uploadFrequency"),
batchSize = getString("batchSize"),
batchProcessingLevel = getString("batchProcessingLevel"),
proxyConfiguration = getMap("proxyConfiguration")?.asProxyConfig(),
firstPartyHosts = getArray("firstPartyHosts")?.asFirstPartyHosts(),
rumConfiguration = rumConfiguration,
logsConfiguration = logsConfiguration,

The Kotlin RumConfiguration data class does not have a firstPartyHosts field. DdSdkConfiguration has it at the top level (line 45), but JS never puts it there in v3:

data class DdSdkConfiguration(
val additionalConfiguration: Map<String, Any?>? = null,
val clientToken: String,
val env: String,
val site: String? = null,
val service: String? = null,
val verbosity: String? = null,
val trackingConsent: String? = null,
val uploadFrequency: String? = null,
val batchSize: String? = null,
val batchProcessingLevel: String? = null,
val proxyConfiguration: Pair<Proxy, ProxyAuthenticator?>? = null,
val firstPartyHosts: Map<String, Set<TracingHeaderType>>? = null,
val rumConfiguration: RumConfiguration? = null,
val logsConfiguration: LogsConfiguration? = null,
val traceConfiguration: TraceConfiguration? = null,
val configurationForTelemetry: ConfigurationForTelemetry? = null
)
/**
* A configuration object for the Datadog RUM feature.
*
* @param applicationId The RUM application ID.
* @param trackFrustrations Whether to track frustration signals or not.
* @param longTaskThresholdMs The threshold for javascript long tasks reporting in milliseconds.
* @param sessionSampleRate The sample rate (between 0 and 100) of RUM sessions kept.
* @param resourceTraceSampleRate Percentage (0–100) of tracing integrations for network calls between your app and your backend.
* @param vitalsUpdateFrequency The frequency to which vitals update are sent (can be 'NEVER', 'RARE', 'AVERAGE' (default), 'FREQUENT').
* @param trackBackgroundEvents Enables/Disables tracking RUM event when no RUM View is active. Might increase number of sessions and billing.
* @param nativeCrashReportEnabled Whether the SDK should track native Android crashes (default is false).
* @param nativeLongTaskThresholdMs The threshold for native long tasks reporting in milliseconds.
* @param nativeViewTracking Enables/Disables tracking RUM Views on the native level.
* @param nativeInteractionTracking Enables/Disables tracking RUM Actions on the native level.
* @param trackNonFatalAnrs Enables tracking of non-fatal ANRs on Android.
* @param initialResourceThreshold The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS).
* @param telemetrySampleRate The sample rate (between 0 and 100) of telemetry events.
* @param customEndpoint Custom RUM intake endpoint used to override the default Datadog intake.
*/
data class RumConfiguration(
val applicationId: String,
val trackFrustrations: Boolean? = null,
val longTaskThresholdMs: Double? = null,
val sessionSampleRate: Double? = null,
val resourceTraceSampleRate: Double? = null,
val vitalsUpdateFrequency: String? = null,
val trackBackgroundEvents: Boolean? = null,
val nativeCrashReportEnabled: Boolean? = null,
val nativeLongTaskThresholdMs: Double? = null,
val nativeViewTracking: Boolean? = null,
val nativeInteractionTracking: Boolean? = null,
val trackNonFatalAnrs: Boolean? = null,
val initialResourceThreshold: Double? = null,
val telemetrySampleRate: Double? = null,
val customEndpoint: String? = null
)

iOS — same bug, reads from root self instead of from within rumConfigurationDict:

let firstPartyHostsArray = self["firstPartyHosts"] as? NSArray
let firstPartyHosts = firstPartyHostsArray?.asFirstPartyHosts()
// MARK: - RUM configuration
let rumConfigurationDict = self["rumConfiguration"] as? NSDictionary
let rumConfiguration: RumConfiguration?
if let rumDict = rumConfigurationDict {


Suggested fix

Android (DdSdkConfigurationExt.kt, line 80) — read from rumMap instead of root:

// Before:
firstPartyHosts = getArray("firstPartyHosts")?.asFirstPartyHosts(),

// After:
firstPartyHosts = rumMap?.getArray("firstPartyHosts")?.asFirstPartyHosts(),

iOS (RNDdSdkConfiguration.swift, line 42) — read from rumConfigurationDict instead of root self:

// Before:
let firstPartyHostsArray = self["firstPartyHosts"] as? NSArray

// After (move inside the rumConfigurationDict block on line 47):
let firstPartyHostsArray = rumConfigurationDict?["firstPartyHosts"] as? NSArray

Reproduction steps

Warning

This was generated with AI, I have not actually try it because we have a problem with #1153

Configure firstPartyHosts inside rumConfiguration (as required by the v3 API):

const config = new CoreConfiguration(clientToken, env, TrackingConsent.GRANTED, {
  site: 'US1',
  rumConfiguration: {
    applicationId: 'your-app-id',
    trackInteractions: true,
    trackResources: true,
    trackErrors: true,
    firstPartyHosts: [
      {
        match: 'api.example.com',
        propagatorTypes: [PropagatorType.DATADOG, PropagatorType.TRACECONTEXT],
      },
    ],
  },
});

await DdSdkReactNative.initialize(config);

Make network requests to api.example.com and observe in APM that no distributed tracing headers are present (x-datadog-trace-id etc. are missing from native requests).

SDK logs

No response

Expected behavior

firstPartyHosts should be read from the rumConfiguration nested object on both Android and iOS, matching where the JS bridge places it.

Affected SDK versions

All v3.x releases: 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0. Also present on the develop branch as of 2026-02-19

Latest working SDK version

2.14.3 — in v2, firstPartyHosts was a top-level property on DdSdkReactNativeConfiguration and was serialized at the top level of the native bridge payload, matching where the native code reads it.

Did you confirm if the latest SDK version fixes the bug?

No

Integration Methods

Yarn

React Native Version

Expo SDK 54 (React Native 0.76)

Package.json Contents

No response

iOS Setup

No response

Android Setup

No response

Device Information

No response

Other relevant information

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions