Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/core/datadog-configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@
"description": "Enables native interaction tracking.",
"type": "boolean"
},
"nativeIosResourceTracking": {
"description": "Controls automatic native network (URLSession) resource tracking. iOS only: has no effect on Android. If not set (default): legacy behavior, enables iOS network tracking if `firstPartyHosts` is defined. `true`: enables iOS network tracking (with trace headers for `firstPartyHosts`, if defined). `false`: disables iOS network tracking.",
"type": "boolean"
},
"nativeCrashReportEnabled": {
"description": "Enables crash reporting for native platforms (iOS, Android). Default `false`.",
"type": "boolean"
Expand Down
7 changes: 7 additions & 0 deletions packages/core/ios/Sources/DdSdkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ public class DdSdkConfiguration: NSObject {
/// - nativeLongTaskThresholdMs: The threshold for reporting native long tasks in milliseconds.
/// - nativeViewTracking: Enables tracking of native iOS/Android UI views.
/// - nativeInteractionTracking: Enables tracking of native UI interactions.
/// - nativeIosResourceTracking: Controls automatic URLSession resource tracking (iOS only).
/// `nil` (default): legacy behavior, tracking enabled iff `firstPartyHosts` is non-nil.
/// `true`: tracking always enabled (trace headers for `firstPartyHosts`, if non-nil).
/// `false`: tracking disabled.
/// - firstPartyHosts: List of backend hosts considered first-party for network tracing.
/// - appHangThreshold: Threshold in seconds for reporting non-fatal app hangs (iOS only).
/// - trackWatchdogTerminations: Whether the SDK should track application terminations
Expand All @@ -115,6 +119,7 @@ public class RumConfiguration: NSObject {
public var nativeLongTaskThresholdMs: Double? = nil
public var nativeViewTracking: Bool? = nil
public var nativeInteractionTracking: Bool? = nil
public var nativeIosResourceTracking: Bool? = nil
public var firstPartyHosts: [String: Set<TracingHeaderType>]? = nil
public var appHangThreshold: Double? = nil
public var trackWatchdogTerminations: Bool
Expand All @@ -135,6 +140,7 @@ public class RumConfiguration: NSObject {
nativeLongTaskThresholdMs: Double? = nil,
nativeViewTracking: Bool?,
nativeInteractionTracking: Bool?,
nativeIosResourceTracking: Bool? = nil,
firstPartyHosts: [String: Set<TracingHeaderType>]?,
appHangThreshold: Double?,
trackWatchdogTerminations: Bool,
Expand All @@ -154,6 +160,7 @@ public class RumConfiguration: NSObject {
self.nativeLongTaskThresholdMs = nativeLongTaskThresholdMs
self.nativeViewTracking = nativeViewTracking
self.nativeInteractionTracking = nativeInteractionTracking
self.nativeIosResourceTracking = nativeIosResourceTracking
self.firstPartyHosts = firstPartyHosts
self.appHangThreshold = appHangThreshold
self.trackWatchdogTerminations = trackWatchdogTerminations
Expand Down
24 changes: 17 additions & 7 deletions packages/core/ios/Sources/DdSdkNativeInitialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,25 @@ public class DdSdkNativeInitialization: NSObject {
uiKitActionsPredicate = DefaultUIKitRUMActionsPredicate()
}

var urlSessionTracking: RUM.Configuration.URLSessionTracking? = nil
// `nativeIosResourceTracking` (iOS only):
// - nil (default): legacy behavior, tracking enabled iff `firstPartyHosts` is non-nil.
// - true: tracking always enabled (trace headers only if `firstPartyHosts` is non-nil).
// - false: tracking disabled.
var firstPartyHostsTracing: RUM.Configuration.URLSessionTracking.FirstPartyHostsTracing? = nil
if let firstPartyHosts = rumConfig.firstPartyHosts {
firstPartyHostsTracing = .traceWithHeaders(
hostsWithHeaders: firstPartyHosts,
sampleRate: Float(
configuration.rumConfiguration?.resourceTraceSampleRate
?? DefaultConfiguration.resourceTraceSampleRate)
)
}
let shouldTrackURLSession = rumConfig.nativeIosResourceTracking ?? (rumConfig.firstPartyHosts != nil)

var urlSessionTracking: RUM.Configuration.URLSessionTracking? = nil
if shouldTrackURLSession {
urlSessionTracking = RUM.Configuration.URLSessionTracking(
firstPartyHostsTracing: .traceWithHeaders(
hostsWithHeaders: firstPartyHosts,
sampleRate: Float(
configuration.rumConfiguration?.resourceTraceSampleRate
?? DefaultConfiguration.resourceTraceSampleRate)
),
firstPartyHostsTracing: firstPartyHostsTracing,
resourceAttributesProvider: { request, _, _, _ in
let trackedBy = request.value(forHTTPHeaderField: InternalConfigurationAttributes.trackedByHeaderKey)
if trackedBy == InternalConfigurationAttributes.trackedByHeaderValue {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/ios/Sources/RNDdSdkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ extension NSDictionary {
let nativeLongTaskThresholdMs = rumDict["nativeLongTaskThresholdMs"] as? Double
let nativeViewTracking = rumDict["nativeViewTracking"] as? Bool
let nativeInteractionTracking = rumDict["nativeInteractionTracking"] as? Bool
// No default on purpose: nil means legacy URLSession tracking behavior
let nativeIosResourceTracking = rumDict["nativeIosResourceTracking"] as? Bool

let firstPartyHostsArray = rumDict["firstPartyHosts"] as? NSArray
let firstPartyHosts = firstPartyHostsArray?.asFirstPartyHosts()
Expand Down Expand Up @@ -89,6 +91,7 @@ extension NSDictionary {
nativeViewTracking: nativeViewTracking ?? DefaultConfiguration.nativeViewTracking,
nativeInteractionTracking: nativeInteractionTracking
?? DefaultConfiguration.nativeInteractionTracking,
nativeIosResourceTracking: nativeIosResourceTracking,
firstPartyHosts: firstPartyHosts,
appHangThreshold: appHangThreshold,
trackWatchdogTerminations: trackWatchdogTerminations
Expand Down Expand Up @@ -378,6 +381,8 @@ extension Dictionary where Key == String, Value == AnyObject {
?? DefaultConfiguration.nativeViewTracking,
nativeInteractionTracking: rum["nativeInteractionTracking"] as? Bool
?? DefaultConfiguration.nativeInteractionTracking,
// No default on purpose: nil means legacy URLSession tracking behavior
nativeIosResourceTracking: rum["nativeIosResourceTracking"] as? Bool,
firstPartyHosts: firstPartyHosts,
appHangThreshold: rum["appHangThreshold"] as? Double,
trackWatchdogTerminations: rum["trackWatchdogTerminations"] as? Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class DdSdkNativeInitializationTests: XCTestCase {
XCTAssertEqual(configuration?.rumConfiguration?.nativeLongTaskThresholdMs, 333.0)
XCTAssertEqual(configuration?.rumConfiguration?.nativeViewTracking, true)
XCTAssertEqual(configuration?.rumConfiguration?.nativeInteractionTracking, true)
XCTAssertEqual(configuration?.rumConfiguration?.nativeIosResourceTracking, true)
XCTAssertEqual(configuration?.verbosity, "WARN")
XCTAssertEqual(configuration?.service, "my.app")
XCTAssertEqual(configuration?.proxyConfiguration?["HTTPEnable"] as? Int, 1)
Expand Down Expand Up @@ -98,6 +99,7 @@ class DdSdkNativeInitializationTests: XCTestCase {
XCTAssertEqual(configuration?.rumConfiguration?.nativeLongTaskThresholdMs, 200.0)
XCTAssertEqual(configuration?.rumConfiguration?.nativeViewTracking, false)
XCTAssertEqual(configuration?.rumConfiguration?.nativeInteractionTracking, false)
XCTAssertNil(configuration?.rumConfiguration?.nativeIosResourceTracking)
XCTAssertEqual(configuration?.verbosity, nil)
XCTAssertEqual(configuration?.service, nil)
XCTAssertNil(configuration?.proxyConfiguration)
Expand Down
95 changes: 95 additions & 0 deletions packages/core/ios/Tests/DdSdkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,101 @@ class DdSdkTests: XCTestCase {
XCTAssertEqual(actualTraceContextInjection, .sampled)
}

// MARK: - nativeIosResourceTracking (URLSession tracking tri-state)

private func buildURLSessionTracking(
nativeIosResourceTracking: Bool?,
firstPartyHosts: [String: Set<TracingHeaderType>]?
) -> RUM.Configuration.URLSessionTracking? {
let rumConfiguration: RumConfiguration = makeDefaultRumConfiguration()
rumConfiguration.resourceTraceSampleRate = 66
rumConfiguration.nativeIosResourceTracking = nativeIosResourceTracking
rumConfiguration.firstPartyHosts = firstPartyHosts
let configuration: DdSdkConfiguration = .mockAny(rumConfiguration: rumConfiguration)
return DdSdkNativeInitialization().buildRumConfiguration(
configuration: configuration
).urlSessionTracking
}

/// Returns the hosts of `.traceWithHeaders`, or `nil` when tracing is not configured.
private func tracedHosts(
_ tracking: RUM.Configuration.URLSessionTracking?
) -> [String: Set<TracingHeaderType>]? {
switch tracking?.firstPartyHostsTracing {
case let .traceWithHeaders(hostsWithHeaders, samplingRate, _):
XCTAssertEqual(samplingRate, 66)
return hostsWithHeaders
case .trace, .none:
return nil
}
}

private let nonEmptyFirstPartyHosts: [String: Set<TracingHeaderType>] = [
"example.com": [.datadog, .tracecontext]
]

// Legacy (nil) behavior: must be unchanged

func testNativeIosResourceTrackingNilWithEmptyFirstPartyHostsEnablesTracking() {
// Regression: JS default `firstPartyHosts: []` => URLSession tracking ON
let tracking = buildURLSessionTracking(nativeIosResourceTracking: nil, firstPartyHosts: [:])
XCTAssertNotNil(tracking)
XCTAssertNotNil(tracking?.resourceAttributesProvider)
XCTAssertEqual(tracedHosts(tracking), [:])
}

func testNativeIosResourceTrackingNilWithNilFirstPartyHostsDisablesTracking() {
let tracking = buildURLSessionTracking(nativeIosResourceTracking: nil, firstPartyHosts: nil)
XCTAssertNil(tracking)
}

func testNativeIosResourceTrackingNilWithFirstPartyHostsTracesHosts() {
let tracking = buildURLSessionTracking(
nativeIosResourceTracking: nil, firstPartyHosts: nonEmptyFirstPartyHosts)
XCTAssertNotNil(tracking)
XCTAssertNotNil(tracking?.resourceAttributesProvider)
XCTAssertEqual(tracedHosts(tracking), nonEmptyFirstPartyHosts)
}

// Explicit opt-in

func testNativeIosResourceTrackingTrueWithNilFirstPartyHostsEnablesTrackingWithoutTracing() {
let tracking = buildURLSessionTracking(nativeIosResourceTracking: true, firstPartyHosts: nil)
XCTAssertNotNil(tracking)
XCTAssertNotNil(tracking?.resourceAttributesProvider)
XCTAssertNil(tracking?.firstPartyHostsTracing)
}

func testNativeIosResourceTrackingTrueWithEmptyFirstPartyHostsEnablesTracking() {
let tracking = buildURLSessionTracking(nativeIosResourceTracking: true, firstPartyHosts: [:])
XCTAssertNotNil(tracking)
XCTAssertNotNil(tracking?.resourceAttributesProvider)
XCTAssertEqual(tracedHosts(tracking), [:])
}

func testNativeIosResourceTrackingTrueWithFirstPartyHostsTracesHosts() {
let tracking = buildURLSessionTracking(
nativeIosResourceTracking: true, firstPartyHosts: nonEmptyFirstPartyHosts)
XCTAssertNotNil(tracking)
XCTAssertNotNil(tracking?.resourceAttributesProvider)
XCTAssertEqual(tracedHosts(tracking), nonEmptyFirstPartyHosts)
}

// Explicit opt-out

func testNativeIosResourceTrackingFalseWithNilFirstPartyHostsDisablesTracking() {
XCTAssertNil(buildURLSessionTracking(nativeIosResourceTracking: false, firstPartyHosts: nil))
}

func testNativeIosResourceTrackingFalseWithEmptyFirstPartyHostsDisablesTracking() {
XCTAssertNil(buildURLSessionTracking(nativeIosResourceTracking: false, firstPartyHosts: [:]))
}

func testNativeIosResourceTrackingFalseWithFirstPartyHostsDisablesTracking() {
XCTAssertNil(buildURLSessionTracking(
nativeIosResourceTracking: false, firstPartyHosts: nonEmptyFirstPartyHosts))
}

func testBuildTelemetrySampleRate() {
let rumConfiguration: RumConfiguration = makeDefaultRumConfiguration()
rumConfiguration.telemetrySampleRate = 42.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"nativeCrashReportEnabled": true,
"nativeLongTaskThresholdMs": 333,
"nativeInteractionTracking": true,
"nativeIosResourceTracking": true,
"actionNameAttribute": "action-name-attr",
"customEndpoint": "https://rum.example.com",
"vitalsUpdateFrequency": "NEVER",
Expand Down
33 changes: 33 additions & 0 deletions packages/core/ios/Tests/RNDdSdkConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,39 @@ class RNDdSdkConfigurationTests: XCTestCase {
XCTAssertEqual(proxy?["SOCKSPort"] as? NSNumber, 99)
}

private func makeConfigurationDict(nativeIosResourceTracking: Any?) -> NSDictionary {
var rumDict: [String: Any] = ["applicationId": "app-id"]
if let value = nativeIosResourceTracking {
rumDict["nativeIosResourceTracking"] = value
}
return [
"clientToken": "client-token",
"env": "env",
"rumConfiguration": rumDict as NSDictionary
]
}

func testBuildNativeIosResourceTrackingAbsentIsNil() {
let configuration = makeConfigurationDict(nativeIosResourceTracking: nil).asDdSdkConfiguration()
XCTAssertNotNil(configuration.rumConfiguration)
XCTAssertNil(configuration.rumConfiguration?.nativeIosResourceTracking)
}

func testBuildNativeIosResourceTrackingNullIsNil() {
let configuration = makeConfigurationDict(nativeIosResourceTracking: NSNull()).asDdSdkConfiguration()
XCTAssertNil(configuration.rumConfiguration?.nativeIosResourceTracking)
}

func testBuildNativeIosResourceTrackingTrue() {
let configuration = makeConfigurationDict(nativeIosResourceTracking: true).asDdSdkConfiguration()
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTracking, true)
}

func testBuildNativeIosResourceTrackingFalse() {
let configuration = makeConfigurationDict(nativeIosResourceTracking: false).asDdSdkConfiguration()
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTracking, false)
}

func testBuildFirstPartyHosts() {
let firstPartyHosts = [
["match": "example.com", "propagatorTypes": ["datadog", "b3"]],
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/__tests__/DdSdkReactNative.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,49 @@ describe('DdSdkReactNative', () => {
).toBe(false);
});

it('initializes with nativeIosResourceTracking undefined when not specified', async () => {
// GIVEN
const configuration = new CoreConfiguration('2', 'env');
configuration.rumConfiguration = new RumConfiguration('1');

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
const ddSdkConfiguration = NativeModules.DdSdk.initialize.mock
.calls[0][0] as DdSdkNativeConfiguration;
expect(ddSdkConfiguration.rumConfiguration).toBeDefined();
expect(
ddSdkConfiguration.rumConfiguration?.nativeIosResourceTracking
).toBeUndefined();
});

it.each([true, false])(
'initializes with nativeIosResourceTracking %s when it is specified',
async value => {
// GIVEN
const configuration = new CoreConfiguration('2', 'env');
configuration.rumConfiguration = new RumConfiguration(
'1',
false,
false,
false,
{ nativeIosResourceTracking: value }
);

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
const ddSdkConfiguration = NativeModules.DdSdk.initialize.mock
.calls[0][0] as DdSdkNativeConfiguration;
expect(
ddSdkConfiguration.rumConfiguration
?.nativeIosResourceTracking
).toBe(value);
}
);

it('initializes with bundleLogsWithTraces false when it is specified', async () => {
// GIVEN
const fakeAppId = '1';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"longTaskThresholdMs": 0,
"nativeCrashReportEnabled": false,
"nativeInteractionTracking": false,
"nativeIosResourceTracking": undefined,
"nativeLongTaskThresholdMs": 200,
"nativeViewTracking": false,
"resourceEventMapper": null,
Expand Down Expand Up @@ -134,6 +135,7 @@ describe('DdSdkReactNativeConfiguration', () => {
nativeLongTaskThresholdMs: 345,
nativeViewTracking: true,
nativeInteractionTracking: true,
nativeIosResourceTracking: true,
longTaskThresholdMs: 567,
trackFrustrations: true,
trackNonFatalAnrs: true,
Expand Down Expand Up @@ -210,6 +212,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"longTaskThresholdMs": 567,
"nativeCrashReportEnabled": true,
"nativeInteractionTracking": true,
"nativeIosResourceTracking": true,
"nativeLongTaskThresholdMs": 345,
"nativeViewTracking": true,
"resourceEventMapper": [Function],
Expand Down Expand Up @@ -314,6 +317,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"longTaskThresholdMs": false,
"nativeCrashReportEnabled": false,
"nativeInteractionTracking": false,
"nativeIosResourceTracking": undefined,
"nativeLongTaskThresholdMs": 0,
"nativeViewTracking": false,
"resourceEventMapper": null,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/config/FileBasedConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ export const getJSONConfiguration = (
telemetrySampleRate:
configuration.rumConfiguration.telemetrySampleRate,
nativeInteractionTracking:
configuration.rumConfiguration.nativeInteractionTracking
configuration.rumConfiguration.nativeInteractionTracking,
nativeIosResourceTracking:
configuration.rumConfiguration.nativeIosResourceTracking
}
}),
...(configuration.traceConfiguration !== undefined && {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/FileBasedConfiguration.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface JsonConfiguration extends CoreConfigurationOptions {
nativeLongTaskThresholdMs?: number;
nativeViewTracking?: boolean;
nativeInteractionTracking?: boolean;
nativeIosResourceTracking?: boolean;
customEndpoint?: string;
sessionSampleRate?: number;
resourceTraceSampleRate?: number;
Expand Down
Loading
Loading