-
Notifications
You must be signed in to change notification settings - Fork 55
Description
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 CoreConfiguration → rumConfiguration 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 side — DdSdkNativeConfiguration (the class serialized over the bridge) has no top-level firstPartyHosts. It lives inside rumConfiguration → RumNativeConfiguration:
dd-sdk-reactnative/packages/core/src/config/features/CoreConfigurationNative.ts
Lines 16 to 38 in fbb259d
| 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:
Lines 24 to 82 in fbb259d
| 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:
Lines 33 to 87 in fbb259d
| 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? NSArrayReproduction 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
- Separate from Missing "API requests", "Actions", and "Other resources" in DataDog RUM after migration to V3.X #1153 (missing actions/resources/errors in v3) — that affects a different part of the RUM pipeline.
- Separate from Trace Headers Not Injected Despite firstPartyHosts Configuration #1013 (trace headers not injected on v2 with
DatadogProviderdouble-init) — that was a JS-side issue whereenableFeatureswasn't re-called with updated config. This bug is a native bridge deserialization issue affecting all standard v3 single-initialization setups.