Skip to content
Open
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
7 changes: 7 additions & 0 deletions packages/core/datadog-configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@
"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"
},
"nativeIosResourceTrackingDisallowList": {
"description": "URL patterns excluded from automatic native iOS network (URLSession) resource tracking. Patterns are matched against the full URL: plain strings match exactly, `*` matches any characters. Patterns without any literal characters (e.g. `\"*\"`) are ignored. Matching requests also get no trace headers injected and no APM span. Only takes effect when native iOS resource tracking is enabled (see `nativeIosResourceTracking`); it never enables tracking by itself. If set while tracking is disabled, the native SDK logs a warning (subject to `verbosity`). iOS only: has no effect on Android. Default: []",
"type": "array",
"items": {
"type": "string"
}
},
"nativeCrashReportEnabled": {
"description": "Enables crash reporting for native platforms (iOS, Android). Default `false`.",
"type": "boolean"
Expand Down
6 changes: 6 additions & 0 deletions packages/core/ios/Sources/DdSdkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ public class DdSdkConfiguration: NSObject {
/// `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.
/// - nativeIosResourceTrackingDisallowList: URL patterns excluded from automatic URLSession resource tracking
/// (iOS only). Passed to `RUM.Configuration.URLSessionTracking.disallowList`; only effective when
/// URLSession tracking is enabled. Default: `[]`.
/// - 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 @@ -120,6 +123,7 @@ public class RumConfiguration: NSObject {
public var nativeViewTracking: Bool? = nil
public var nativeInteractionTracking: Bool? = nil
public var nativeIosResourceTracking: Bool? = nil
public var nativeIosResourceTrackingDisallowList: [String] = []
public var firstPartyHosts: [String: Set<TracingHeaderType>]? = nil
public var appHangThreshold: Double? = nil
public var trackWatchdogTerminations: Bool
Expand All @@ -141,6 +145,7 @@ public class RumConfiguration: NSObject {
nativeViewTracking: Bool?,
nativeInteractionTracking: Bool?,
nativeIosResourceTracking: Bool? = nil,
nativeIosResourceTrackingDisallowList: [String] = [],
firstPartyHosts: [String: Set<TracingHeaderType>]?,
appHangThreshold: Double?,
trackWatchdogTerminations: Bool,
Expand All @@ -161,6 +166,7 @@ public class RumConfiguration: NSObject {
self.nativeViewTracking = nativeViewTracking
self.nativeInteractionTracking = nativeInteractionTracking
self.nativeIosResourceTracking = nativeIosResourceTracking
self.nativeIosResourceTrackingDisallowList = nativeIosResourceTrackingDisallowList
self.firstPartyHosts = firstPartyHosts
self.appHangThreshold = appHangThreshold
self.trackWatchdogTerminations = trackWatchdogTerminations
Expand Down
10 changes: 9 additions & 1 deletion packages/core/ios/Sources/DdSdkNativeInitialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ public class DdSdkNativeInitialization: NSObject {
// - 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.
// `nativeIosResourceTrackingDisallowList` is applied only when tracking is enabled; it never enables it.
var firstPartyHostsTracing: RUM.Configuration.URLSessionTracking.FirstPartyHostsTracing? = nil
if let firstPartyHosts = rumConfig.firstPartyHosts {
firstPartyHostsTracing = .traceWithHeaders(
Expand All @@ -193,7 +194,14 @@ public class DdSdkNativeInitialization: NSObject {
return [InternalConfigurationAttributes.dropResource: true]
}
return nil
}
},
disallowList: rumConfig.nativeIosResourceTrackingDisallowList
)
} else if !rumConfig.nativeIosResourceTrackingDisallowList.isEmpty {
DD.logger.warn(
"nativeIosResourceTrackingDisallowList is set but native iOS resource tracking is disabled "
+ "(nativeIosResourceTracking is false, or unset with no firstPartyHosts). "
+ "The disallow list will be ignored."
)
}

Expand Down
4 changes: 4 additions & 0 deletions packages/core/ios/Sources/RNDdSdkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ extension NSDictionary {
let nativeInteractionTracking = rumDict["nativeInteractionTracking"] as? Bool
// No default on purpose: nil means legacy URLSession tracking behavior
let nativeIosResourceTracking = rumDict["nativeIosResourceTracking"] as? Bool
let nativeIosResourceTrackingDisallowList = rumDict["nativeIosResourceTrackingDisallowList"] as? [String]

let firstPartyHostsArray = rumDict["firstPartyHosts"] as? NSArray
let firstPartyHosts = firstPartyHostsArray?.asFirstPartyHosts()
Expand Down Expand Up @@ -92,6 +93,7 @@ extension NSDictionary {
nativeInteractionTracking: nativeInteractionTracking
?? DefaultConfiguration.nativeInteractionTracking,
nativeIosResourceTracking: nativeIosResourceTracking,
nativeIosResourceTrackingDisallowList: nativeIosResourceTrackingDisallowList ?? [],
firstPartyHosts: firstPartyHosts,
appHangThreshold: appHangThreshold,
trackWatchdogTerminations: trackWatchdogTerminations
Expand Down Expand Up @@ -383,6 +385,8 @@ extension Dictionary where Key == String, Value == AnyObject {
?? DefaultConfiguration.nativeInteractionTracking,
// No default on purpose: nil means legacy URLSession tracking behavior
nativeIosResourceTracking: rum["nativeIosResourceTracking"] as? Bool,
nativeIosResourceTrackingDisallowList:
(rum["nativeIosResourceTrackingDisallowList"] as? [String]) ?? [],
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 @@ -57,6 +57,8 @@ class DdSdkNativeInitializationTests: XCTestCase {
XCTAssertEqual(configuration?.rumConfiguration?.nativeViewTracking, true)
XCTAssertEqual(configuration?.rumConfiguration?.nativeInteractionTracking, true)
XCTAssertEqual(configuration?.rumConfiguration?.nativeIosResourceTracking, true)
XCTAssertEqual(
configuration?.rumConfiguration?.nativeIosResourceTrackingDisallowList, ["https://3p.example.com/*"])
XCTAssertEqual(configuration?.verbosity, "WARN")
XCTAssertEqual(configuration?.service, "my.app")
XCTAssertEqual(configuration?.proxyConfiguration?["HTTPEnable"] as? Int, 1)
Expand Down Expand Up @@ -100,6 +102,7 @@ class DdSdkNativeInitializationTests: XCTestCase {
XCTAssertEqual(configuration?.rumConfiguration?.nativeViewTracking, false)
XCTAssertEqual(configuration?.rumConfiguration?.nativeInteractionTracking, false)
XCTAssertNil(configuration?.rumConfiguration?.nativeIosResourceTracking)
XCTAssertEqual(configuration?.rumConfiguration?.nativeIosResourceTrackingDisallowList, [])
XCTAssertEqual(configuration?.verbosity, nil)
XCTAssertEqual(configuration?.service, nil)
XCTAssertNil(configuration?.proxyConfiguration)
Expand Down
88 changes: 87 additions & 1 deletion packages/core/ios/Tests/DdSdkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -997,11 +997,13 @@ class DdSdkTests: XCTestCase {

private func buildURLSessionTracking(
nativeIosResourceTracking: Bool?,
firstPartyHosts: [String: Set<TracingHeaderType>]?
firstPartyHosts: [String: Set<TracingHeaderType>]?,
disallowList: [String] = []
) -> RUM.Configuration.URLSessionTracking? {
let rumConfiguration: RumConfiguration = makeDefaultRumConfiguration()
rumConfiguration.resourceTraceSampleRate = 66
rumConfiguration.nativeIosResourceTracking = nativeIosResourceTracking
rumConfiguration.nativeIosResourceTrackingDisallowList = disallowList
rumConfiguration.firstPartyHosts = firstPartyHosts
let configuration: DdSdkConfiguration = .mockAny(rumConfiguration: rumConfiguration)
return DdSdkNativeInitialization().buildRumConfiguration(
Expand Down Expand Up @@ -1088,6 +1090,82 @@ class DdSdkTests: XCTestCase {
nativeIosResourceTracking: false, firstPartyHosts: nonEmptyFirstPartyHosts))
}

// MARK: - nativeIosResourceTrackingDisallowList

private let disallowList = ["https://3p.example.com/*", "https://cdn.example.com/a"]

/// Runs `block` with `DD.logger` replaced by a recording logger, restoring the original afterwards.
private func withRecordingLogger(_ block: () -> Void) -> [(level: CoreLoggerLevel, message: String)] {
let recordingLogger = RecordingCoreLogger()
let originalLogger = DD.logger
DD.logger = recordingLogger
defer { DD.logger = originalLogger }
block()
return recordingLogger.recordedLogs
}

private func disallowListWarnings(
_ logs: [(level: CoreLoggerLevel, message: String)]
) -> [(level: CoreLoggerLevel, message: String)] {
return logs.filter { $0.level == .warn && $0.message.contains("nativeIosResourceTrackingDisallowList") }
}

func testDisallowListIsPassedWhenTrackingExplicitlyEnabled() {
let tracking = buildURLSessionTracking(
nativeIosResourceTracking: true, firstPartyHosts: nil, disallowList: disallowList)
XCTAssertNotNil(tracking)
XCTAssertEqual(tracking?.disallowList, disallowList)
}

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

func testDisallowListDefaultsToEmptyWhenTrackingEnabled() {
let tracking = buildURLSessionTracking(nativeIosResourceTracking: true, firstPartyHosts: nil)
XCTAssertNotNil(tracking)
XCTAssertEqual(tracking?.disallowList, [])
}

func testDisallowListDoesNotEnableTrackingWhenExplicitlyDisabled() {
var tracking: RUM.Configuration.URLSessionTracking?
let logs = withRecordingLogger {
tracking = buildURLSessionTracking(
nativeIosResourceTracking: false, firstPartyHosts: nil, disallowList: disallowList)
}
XCTAssertNil(tracking)
XCTAssertEqual(disallowListWarnings(logs).count, 1)
}

func testDisallowListDoesNotEnableTrackingWhenLegacyDisabled() {
var tracking: RUM.Configuration.URLSessionTracking?
let logs = withRecordingLogger {
tracking = buildURLSessionTracking(
nativeIosResourceTracking: nil, firstPartyHosts: nil, disallowList: disallowList)
}
XCTAssertNil(tracking)
XCTAssertEqual(disallowListWarnings(logs).count, 1)
}

func testNoDisallowListWarningWhenTrackingDisabledAndListEmpty() {
let logs = withRecordingLogger {
XCTAssertNil(buildURLSessionTracking(nativeIosResourceTracking: false, firstPartyHosts: nil))
}
XCTAssertTrue(disallowListWarnings(logs).isEmpty)
}

func testNoDisallowListWarningWhenTrackingEnabled() {
let logs = withRecordingLogger {
XCTAssertNotNil(buildURLSessionTracking(
nativeIosResourceTracking: true, firstPartyHosts: nil, disallowList: disallowList))
}
XCTAssertTrue(disallowListWarnings(logs).isEmpty)
}

func testBuildTelemetrySampleRate() {
let rumConfiguration: RumConfiguration = makeDefaultRumConfiguration()
rumConfiguration.telemetrySampleRate = 42.0
Expand Down Expand Up @@ -2035,3 +2113,11 @@ class MockOnSdkInitializedListener {
self.receivedCore = core
}
}

private final class RecordingCoreLogger: CoreLogger {
private(set) var recordedLogs: [(level: CoreLoggerLevel, message: String)] = []

func log(_ level: CoreLoggerLevel, message: @autoclosure () -> String, error: Error?) {
recordedLogs.append((level: level, message: message()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"nativeLongTaskThresholdMs": 333,
"nativeInteractionTracking": true,
"nativeIosResourceTracking": true,
"nativeIosResourceTrackingDisallowList": ["https://3p.example.com/*"],
"actionNameAttribute": "action-name-attr",
"customEndpoint": "https://rum.example.com",
"vitalsUpdateFrequency": "NEVER",
Expand Down
28 changes: 27 additions & 1 deletion packages/core/ios/Tests/RNDdSdkConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ class RNDdSdkConfigurationTests: XCTestCase {
XCTAssertEqual(proxy?["SOCKSPort"] as? NSNumber, 99)
}

private func makeConfigurationDict(nativeIosResourceTracking: Any?) -> NSDictionary {
private func makeConfigurationDict(nativeIosResourceTracking: Any?, disallowList: Any? = nil) -> NSDictionary {
var rumDict: [String: Any] = ["applicationId": "app-id"]
if let value = nativeIosResourceTracking {
rumDict["nativeIosResourceTracking"] = value
}
if let value = disallowList {
rumDict["nativeIosResourceTrackingDisallowList"] = value
}
return [
"clientToken": "client-token",
"env": "env",
Expand Down Expand Up @@ -92,6 +95,29 @@ class RNDdSdkConfigurationTests: XCTestCase {
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTracking, false)
}

func testBuildNativeIosResourceTrackingDisallowListAbsentIsEmpty() {
let configuration = makeConfigurationDict(nativeIosResourceTracking: nil).asDdSdkConfiguration()
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTrackingDisallowList, [])
}

func testBuildNativeIosResourceTrackingDisallowListNullIsEmpty() {
let configuration = makeConfigurationDict(
nativeIosResourceTracking: nil, disallowList: NSNull()).asDdSdkConfiguration()
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTrackingDisallowList, [])
}

func testBuildNativeIosResourceTrackingDisallowList() {
let configuration = makeConfigurationDict(
nativeIosResourceTracking: nil, disallowList: ["a/*", "b"] as NSArray).asDdSdkConfiguration()
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTrackingDisallowList, ["a/*", "b"])
}

func testBuildNativeIosResourceTrackingDisallowListWrongTypeIsEmpty() {
let configuration = makeConfigurationDict(
nativeIosResourceTracking: nil, disallowList: "a").asDdSdkConfiguration()
XCTAssertEqual(configuration.rumConfiguration?.nativeIosResourceTrackingDisallowList, [])
}

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

it('initializes with nativeIosResourceTrackingDisallowList 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
?.nativeIosResourceTrackingDisallowList
).toBeUndefined();
});

it('initializes with nativeIosResourceTrackingDisallowList when it is specified', async () => {
// GIVEN
const disallowList = [
'https://3p.example.com/*',
'https://cdn.example.com/a'
];
const configuration = new CoreConfiguration('2', 'env');
configuration.rumConfiguration = new RumConfiguration(
'1',
false,
false,
false,
{ nativeIosResourceTrackingDisallowList: disallowList }
);

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
const ddSdkConfiguration = NativeModules.DdSdk.initialize.mock
.calls[0][0] as DdSdkNativeConfiguration;
expect(
ddSdkConfiguration.rumConfiguration
?.nativeIosResourceTrackingDisallowList
).toEqual(disallowList);
});

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 @@ -66,6 +66,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"nativeCrashReportEnabled": false,
"nativeInteractionTracking": false,
"nativeIosResourceTracking": undefined,
"nativeIosResourceTrackingDisallowList": undefined,
"nativeLongTaskThresholdMs": 200,
"nativeViewTracking": false,
"resourceEventMapper": null,
Expand Down Expand Up @@ -136,6 +137,9 @@ describe('DdSdkReactNativeConfiguration', () => {
nativeViewTracking: true,
nativeInteractionTracking: true,
nativeIosResourceTracking: true,
nativeIosResourceTrackingDisallowList: [
'https://3p.example.com/*'
],
longTaskThresholdMs: 567,
trackFrustrations: true,
trackNonFatalAnrs: true,
Expand Down Expand Up @@ -213,6 +217,9 @@ describe('DdSdkReactNativeConfiguration', () => {
"nativeCrashReportEnabled": true,
"nativeInteractionTracking": true,
"nativeIosResourceTracking": true,
"nativeIosResourceTrackingDisallowList": [
"https://3p.example.com/*",
],
"nativeLongTaskThresholdMs": 345,
"nativeViewTracking": true,
"resourceEventMapper": [Function],
Expand Down Expand Up @@ -318,6 +325,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"nativeCrashReportEnabled": false,
"nativeInteractionTracking": false,
"nativeIosResourceTracking": undefined,
"nativeIosResourceTrackingDisallowList": undefined,
"nativeLongTaskThresholdMs": 0,
"nativeViewTracking": false,
"resourceEventMapper": null,
Expand Down
Loading
Loading