From eec90c61a53086319d744f956fa03cb751b8fa3e Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Thu, 1 Oct 2026 13:34:10 +0200 Subject: [PATCH] feat: expose iOS native resource tracking disallow-list --- .../core/datadog-configuration.schema.json | 7 ++ .../core/ios/Sources/DdSdkConfiguration.swift | 6 ++ .../Sources/DdSdkNativeInitialization.swift | 10 ++- .../ios/Sources/RNDdSdkConfiguration.swift | 4 + .../DdSdkNativeInitializationTests.swift | 3 + packages/core/ios/Tests/DdSdkTests.swift | 88 ++++++++++++++++++- .../Fixtures/complete-configuration.json | 1 + .../ios/Tests/RNDdSdkConfigurationTests.swift | 28 +++++- .../src/__tests__/DdSdkReactNative.test.tsx | 45 ++++++++++ .../DdSdkReactNativeConfiguration.test.ts | 8 ++ .../core/src/config/FileBasedConfiguration.ts | 5 +- .../src/config/FileBasedConfiguration.type.ts | 1 + .../__tests__/FileBasedConfiguration.test.ts | 41 +++++++++ .../configuration-all-fields.json | 1 + .../async/AutoInstrumentationConfiguration.ts | 4 + .../PartialInitializationConfiguration.ts | 1 + .../src/config/features/RumConfiguration.ts | 5 ++ .../config/features/RumConfiguration.type.ts | 15 ++++ .../config/features/RumConfigurationNative.ts | 1 + .../__tests__/initialization.test.tsx | 83 +++++++++++++++++ 20 files changed, 353 insertions(+), 4 deletions(-) diff --git a/packages/core/datadog-configuration.schema.json b/packages/core/datadog-configuration.schema.json index 4d83a3de8..c06bc5de4 100644 --- a/packages/core/datadog-configuration.schema.json +++ b/packages/core/datadog-configuration.schema.json @@ -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" diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 2f1a056df..b91ff8b1e 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -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 @@ -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]? = nil public var appHangThreshold: Double? = nil public var trackWatchdogTerminations: Bool @@ -141,6 +145,7 @@ public class RumConfiguration: NSObject { nativeViewTracking: Bool?, nativeInteractionTracking: Bool?, nativeIosResourceTracking: Bool? = nil, + nativeIosResourceTrackingDisallowList: [String] = [], firstPartyHosts: [String: Set]?, appHangThreshold: Double?, trackWatchdogTerminations: Bool, @@ -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 diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 4199af491..db8ed7647 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -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( @@ -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." ) } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 0ab0cfd21..8e23d5b7d 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -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() @@ -92,6 +93,7 @@ extension NSDictionary { nativeInteractionTracking: nativeInteractionTracking ?? DefaultConfiguration.nativeInteractionTracking, nativeIosResourceTracking: nativeIosResourceTracking, + nativeIosResourceTrackingDisallowList: nativeIosResourceTrackingDisallowList ?? [], firstPartyHosts: firstPartyHosts, appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations @@ -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 diff --git a/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift b/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift index 712bb00d3..a97298d38 100644 --- a/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift +++ b/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift @@ -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) @@ -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) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index f47300ff6..1af69a753 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -997,11 +997,13 @@ class DdSdkTests: XCTestCase { private func buildURLSessionTracking( nativeIosResourceTracking: Bool?, - firstPartyHosts: [String: Set]? + firstPartyHosts: [String: Set]?, + 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( @@ -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 @@ -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())) + } +} diff --git a/packages/core/ios/Tests/Fixtures/complete-configuration.json b/packages/core/ios/Tests/Fixtures/complete-configuration.json index d41df0a6b..db4b624bc 100644 --- a/packages/core/ios/Tests/Fixtures/complete-configuration.json +++ b/packages/core/ios/Tests/Fixtures/complete-configuration.json @@ -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", diff --git a/packages/core/ios/Tests/RNDdSdkConfigurationTests.swift b/packages/core/ios/Tests/RNDdSdkConfigurationTests.swift index 027aaf2e0..504755711 100644 --- a/packages/core/ios/Tests/RNDdSdkConfigurationTests.swift +++ b/packages/core/ios/Tests/RNDdSdkConfigurationTests.swift @@ -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", @@ -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"]], diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index de6013c1a..a8d975dc7 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -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'; diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index ec6ecd777..295bf5011 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -66,6 +66,7 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeCrashReportEnabled": false, "nativeInteractionTracking": false, "nativeIosResourceTracking": undefined, + "nativeIosResourceTrackingDisallowList": undefined, "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "resourceEventMapper": null, @@ -136,6 +137,9 @@ describe('DdSdkReactNativeConfiguration', () => { nativeViewTracking: true, nativeInteractionTracking: true, nativeIosResourceTracking: true, + nativeIosResourceTrackingDisallowList: [ + 'https://3p.example.com/*' + ], longTaskThresholdMs: 567, trackFrustrations: true, trackNonFatalAnrs: true, @@ -213,6 +217,9 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeCrashReportEnabled": true, "nativeInteractionTracking": true, "nativeIosResourceTracking": true, + "nativeIosResourceTrackingDisallowList": [ + "https://3p.example.com/*", + ], "nativeLongTaskThresholdMs": 345, "nativeViewTracking": true, "resourceEventMapper": [Function], @@ -318,6 +325,7 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeCrashReportEnabled": false, "nativeInteractionTracking": false, "nativeIosResourceTracking": undefined, + "nativeIosResourceTrackingDisallowList": undefined, "nativeLongTaskThresholdMs": 0, "nativeViewTracking": false, "resourceEventMapper": null, diff --git a/packages/core/src/config/FileBasedConfiguration.ts b/packages/core/src/config/FileBasedConfiguration.ts index 8b9d20e52..508261580 100644 --- a/packages/core/src/config/FileBasedConfiguration.ts +++ b/packages/core/src/config/FileBasedConfiguration.ts @@ -210,7 +210,10 @@ export const getJSONConfiguration = ( nativeInteractionTracking: configuration.rumConfiguration.nativeInteractionTracking, nativeIosResourceTracking: - configuration.rumConfiguration.nativeIosResourceTracking + configuration.rumConfiguration.nativeIosResourceTracking, + nativeIosResourceTrackingDisallowList: + configuration.rumConfiguration + .nativeIosResourceTrackingDisallowList } }), ...(configuration.traceConfiguration !== undefined && { diff --git a/packages/core/src/config/FileBasedConfiguration.type.ts b/packages/core/src/config/FileBasedConfiguration.type.ts index d382e5dfa..1fb8cff33 100644 --- a/packages/core/src/config/FileBasedConfiguration.type.ts +++ b/packages/core/src/config/FileBasedConfiguration.type.ts @@ -47,6 +47,7 @@ export interface JsonConfiguration extends CoreConfigurationOptions { nativeViewTracking?: boolean; nativeInteractionTracking?: boolean; nativeIosResourceTracking?: boolean; + nativeIosResourceTrackingDisallowList?: string[]; customEndpoint?: string; sessionSampleRate?: number; resourceTraceSampleRate?: number; diff --git a/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts index 2240a490e..93fe175c9 100644 --- a/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts @@ -60,6 +60,9 @@ describe('FileBasedConfiguration', () => { "nativeCrashReportEnabled": true, "nativeInteractionTracking": true, "nativeIosResourceTracking": true, + "nativeIosResourceTrackingDisallowList": [ + "https://3p.example.com/*", + ], "nativeLongTaskThresholdMs": 789, "nativeViewTracking": true, "resourceEventMapper": null, @@ -174,6 +177,7 @@ describe('FileBasedConfiguration', () => { "nativeCrashReportEnabled": false, "nativeInteractionTracking": false, "nativeIosResourceTracking": undefined, + "nativeIosResourceTrackingDisallowList": undefined, "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "resourceEventMapper": null, @@ -240,6 +244,7 @@ describe('FileBasedConfiguration', () => { "nativeCrashReportEnabled": false, "nativeInteractionTracking": false, "nativeIosResourceTracking": undefined, + "nativeIosResourceTrackingDisallowList": undefined, "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "resourceEventMapper": null, @@ -305,6 +310,42 @@ describe('FileBasedConfiguration', () => { } ); + it('leaves nativeIosResourceTrackingDisallowList undefined when not set', () => { + const config = new FileBasedConfiguration({ + configuration: { + env: 'fake-env', + clientToken: 'fake-client-token', + rumConfiguration: { + applicationId: 'fake-app-id' + } + } + }); + expect(config.rumConfiguration).toHaveProperty( + 'nativeIosResourceTrackingDisallowList', + undefined + ); + }); + + it('passes nativeIosResourceTrackingDisallowList through when set', () => { + const disallowList = [ + 'https://3p.example.com/*', + 'https://cdn.example.com/a' + ]; + const config = new FileBasedConfiguration({ + configuration: { + env: 'fake-env', + clientToken: 'fake-client-token', + rumConfiguration: { + applicationId: 'fake-app-id', + nativeIosResourceTrackingDisallowList: disallowList + } + } + }); + expect( + config.rumConfiguration?.nativeIosResourceTrackingDisallowList + ).toEqual(disallowList); + }); + it('applies event mappers to configuration when provided', () => { const actionEventMapper = () => null; const errorEventMapper = () => null; diff --git a/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json b/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json index bfbda7a30..c9f4a434e 100644 --- a/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json +++ b/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json @@ -29,6 +29,7 @@ "nativeViewTracking": true, "nativeInteractionTracking": true, "nativeIosResourceTracking": true, + "nativeIosResourceTrackingDisallowList": ["https://3p.example.com/*"], "nativeLongTaskThresholdMs": 789, "nativeCrashReportEnabled": true, "trackBackgroundEvents": true, diff --git a/packages/core/src/config/async/AutoInstrumentationConfiguration.ts b/packages/core/src/config/async/AutoInstrumentationConfiguration.ts index 2a1c34184..e4fdd787b 100644 --- a/packages/core/src/config/async/AutoInstrumentationConfiguration.ts +++ b/packages/core/src/config/async/AutoInstrumentationConfiguration.ts @@ -29,6 +29,7 @@ export type AutoInstrumentationConfiguration = { readonly nativeLongTaskThresholdMs?: number; readonly nativeViewTracking?: boolean; readonly nativeIosResourceTracking?: boolean; + readonly nativeIosResourceTrackingDisallowList?: string[]; readonly actionEventMapper?: ActionEventMapper | null; readonly errorEventMapper?: ErrorEventMapper | null; readonly resourceEventMapper?: ResourceEventMapper | null; @@ -55,6 +56,7 @@ export type AutoInstrumentationParameters = { readonly nativeLongTaskThresholdMs?: number; readonly nativeViewTracking?: boolean; readonly nativeIosResourceTracking?: boolean; + readonly nativeIosResourceTrackingDisallowList?: string[]; readonly actionEventMapper: ActionEventMapper | null; readonly errorEventMapper: ErrorEventMapper | null; readonly resourceEventMapper: ResourceEventMapper | null; @@ -122,6 +124,8 @@ export const addDefaultValuesToAutoInstrumentationConfiguration = ( // No default on purpose: undefined means legacy iOS behavior nativeIosResourceTracking: features.rumConfiguration.nativeIosResourceTracking, + nativeIosResourceTrackingDisallowList: + features.rumConfiguration.nativeIosResourceTrackingDisallowList, firstPartyHosts: features.rumConfiguration.firstPartyHosts || RUM_DEFAULTS.getFirstPartyHosts() diff --git a/packages/core/src/config/async/PartialInitializationConfiguration.ts b/packages/core/src/config/async/PartialInitializationConfiguration.ts index 44ae2ffa6..c8c293ed0 100644 --- a/packages/core/src/config/async/PartialInitializationConfiguration.ts +++ b/packages/core/src/config/async/PartialInitializationConfiguration.ts @@ -37,6 +37,7 @@ export type PartialInitializationConfiguration = { readonly nativeViewTracking?: boolean; readonly nativeInteractionTracking?: boolean; readonly nativeIosResourceTracking?: boolean; + readonly nativeIosResourceTrackingDisallowList?: string[]; readonly longTaskThresholdMs?: number | false; readonly vitalsUpdateFrequency?: VitalsUpdateFrequency; readonly trackFrustrations?: boolean; diff --git a/packages/core/src/config/features/RumConfiguration.ts b/packages/core/src/config/features/RumConfiguration.ts index af723d21a..2a257b578 100644 --- a/packages/core/src/config/features/RumConfiguration.ts +++ b/packages/core/src/config/features/RumConfiguration.ts @@ -27,6 +27,7 @@ const DEFAULTS = { nativeInteractionTracking: false, nativeLongTaskThresholdMs: 200, nativeIosResourceTracking: undefined, + nativeIosResourceTrackingDisallowList: undefined, nativeViewTracking: false, resourceEventMapper: null, resourceTraceSampleRate: 100.0, @@ -90,6 +91,10 @@ export class RumConfiguration implements RumConfigurationType { public nativeIosResourceTracking?: boolean = DEFAULTS.nativeIosResourceTracking; + // URL patterns excluded from native iOS resource tracking; undefined means [] on iOS + public nativeIosResourceTrackingDisallowList?: string[] = + DEFAULTS.nativeIosResourceTrackingDisallowList; + // Native View Tracking enabled public nativeViewTracking: boolean = DEFAULTS.nativeViewTracking; diff --git a/packages/core/src/config/features/RumConfiguration.type.ts b/packages/core/src/config/features/RumConfiguration.type.ts index 607f507e9..be8cc48e1 100644 --- a/packages/core/src/config/features/RumConfiguration.type.ts +++ b/packages/core/src/config/features/RumConfiguration.type.ts @@ -117,6 +117,21 @@ export interface RumConfigurationOptions { */ nativeIosResourceTracking?: boolean; + /** + * URL patterns excluded from automatic native iOS network (URLSession) resource tracking. + * iOS only: has no effect on Android. + * - 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`). + * + * Default: `[]` (no URLs excluded). + */ + nativeIosResourceTrackingDisallowList?: string[]; + /** * Custom mapper to transform RUM resource events. */ diff --git a/packages/core/src/config/features/RumConfigurationNative.ts b/packages/core/src/config/features/RumConfigurationNative.ts index 71a3e0eda..65f345d4e 100644 --- a/packages/core/src/config/features/RumConfigurationNative.ts +++ b/packages/core/src/config/features/RumConfigurationNative.ts @@ -16,6 +16,7 @@ export type RumNativeConfiguration = { readonly nativeViewTracking: boolean; readonly nativeInteractionTracking: boolean; readonly nativeIosResourceTracking: boolean | undefined; + readonly nativeIosResourceTrackingDisallowList: string[] | undefined; readonly trackNonFatalAnrs: boolean | undefined; readonly appHangThreshold: number | undefined; readonly trackWatchdogTerminations: boolean | undefined; diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index b0a0ed4a4..13ebe6b9e 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -97,6 +97,7 @@ describe('DatadogProvider', () => { "nativeCrashReportEnabled": false, "nativeInteractionTracking": false, "nativeIosResourceTracking": undefined, + "nativeIosResourceTrackingDisallowList": undefined, "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "resourceEventMapper": null, @@ -260,6 +261,88 @@ describe('DatadogProvider', () => { ); }); + describe('nativeIosResourceTrackingDisallowList', () => { + const disallowList = [ + 'https://3p.example.com/*', + 'https://cdn.example.com/a' + ]; + + it('sends nativeIosResourceTrackingDisallowList undefined to native by default', async () => { + renderWithProvider(); + await flushPromises(); + expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1); + const receivedConfiguration = + NativeModules.DdSdk.initialize.mock.calls[0][0]; + expect(receivedConfiguration.rumConfiguration).toBeDefined(); + expect( + receivedConfiguration.rumConfiguration + .nativeIosResourceTrackingDisallowList + ).toBeUndefined(); + }); + + it('sends nativeIosResourceTrackingDisallowList to native when set', async () => { + const configuration = getDefaultConfiguration(); + const rumConfiguration = configuration.rumConfiguration; + if (rumConfiguration) { + rumConfiguration.nativeIosResourceTrackingDisallowList = disallowList; + } + renderWithProvider({ configuration }); + await flushPromises(); + expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1); + const receivedConfiguration = + NativeModules.DdSdk.initialize.mock.calls[0][0]; + expect( + receivedConfiguration.rumConfiguration + .nativeIosResourceTrackingDisallowList + ).toEqual(disallowList); + }); + + it.each([ + ['unset', 'unset', undefined], + ['list', 'unset', disallowList], + ['unset', 'list', disallowList] + ])( + 'partial initialization: features %s, initialize %s => native %p', + async (featuresValue, initializeValue, expected) => { + renderWithProvider({ + configuration: { + rumConfiguration: { + trackErrors: false, + trackResources: false, + trackInteractions: false, + ...(featuresValue === 'list' && { + nativeIosResourceTrackingDisallowList: disallowList + }) + }, + traceConfiguration: {}, + logsConfiguration: {} + } + }); + await flushPromises(); + expect(NativeModules.DdSdk.initialize).not.toHaveBeenCalled(); + + await DatadogProvider.initialize({ + clientToken: 'fake-client-token', + env: 'fake-env', + rumConfiguration: { + applicationId: 'fake-application-id', + ...(initializeValue === 'list' && { + nativeIosResourceTrackingDisallowList: disallowList + }) + } + }); + await flushPromises(); + expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1); + const receivedConfiguration = + NativeModules.DdSdk.initialize.mock.calls[0][0]; + expect( + receivedConfiguration.rumConfiguration + .nativeIosResourceTrackingDisallowList + ).toEqual(expected); + } + ); + }); + describe('onInitialization callback', () => { it('runs after initialization when SYNC initialization', async () => { const onInitialization = jest.fn();