diff --git a/CHANGELOG.md b/CHANGELOG.md index 7401c639ad..757f31ecac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [FEATURE] Add network instrumentation for async/await URLSession APIs. See [#1394][] + # 2.3.0 / 02-10-2023 - [IMPROVEMENT] Add UIBackgroundTask for uploading jobs. See [#1412][] @@ -537,6 +539,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1502]: https://github.com/DataDog/dd-sdk-ios/pull/1502 [#1465]: https://github.com/DataDog/dd-sdk-ios/pull/1465 [#1498]: https://github.com/DataDog/dd-sdk-ios/pull/1498 +[#1394]: https://github.com/DataDog/dd-sdk-ios/pull/1394 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 60f09204f4..833c3e78ea 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -21,6 +21,18 @@ 3C0D5DF02A5442A900446CF9 /* EventMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */; }; 3C0D5DF52A5443B100446CF9 /* DataFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */; }; 3C0D5DF62A5443B100446CF9 /* DataFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */; }; + 3C1890152ABDE9BF00CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */; }; + 3C1890162ABDE9C000CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */; }; + 3C2206F32AB9CE9300DE780C /* MetaTypeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2206F22AB9CE9300DE780C /* MetaTypeExtensions.swift */; }; + 3C2206F42AB9CE9300DE780C /* MetaTypeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2206F22AB9CE9300DE780C /* MetaTypeExtensions.swift */; }; + 3C2206F52AB9DB9000DE780C /* DatadogSessionReplay.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C2206F62AB9DBA700DE780C /* DatadogRUM.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C2206F72AB9DBB600DE780C /* DatadogTrace.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C2206F82AB9DBC600DE780C /* DatadogInternal.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C394EF72AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */; }; + 3C394EF82AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */; }; + 3C394EFA2AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */; }; + 3C394EFB2AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */; }; 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3C74305C29FBC0480053B80F /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; 3C85D42129F7C5C900AFF894 /* WebViewTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */; }; @@ -28,8 +40,32 @@ 3C85D42C29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */; }; 3C85D42D29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */; }; 3C9C6BB429F7C0C000581C43 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + 3CB32AD42ACB733000D602ED /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */; }; + 3CB32AD52ACB733000D602ED /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */; }; + 3CB32AD72ACB735600D602ED /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD62ACB735600D602ED /* URLSessionSwizzlerTests.swift */; }; + 3CB32AD82ACB735600D602ED /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD62ACB735600D602ED /* URLSessionSwizzlerTests.swift */; }; + 3CBDE66E2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE66D2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift */; }; + 3CBDE66F2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE66D2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift */; }; + 3CBDE6712AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6702AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift */; }; + 3CBDE6722AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6702AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift */; }; + 3CBDE6742AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */; }; + 3CBDE6752AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */; }; + 3CBDE6812AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6802AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift */; }; + 3CBDE6822AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6802AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift */; }; + 3CBDE6842AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6832AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift */; }; + 3CBDE6852AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6832AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift */; }; + 3CBDE6872AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6862AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift */; }; + 3CBDE6882AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6862AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift */; }; + 3CBDE68A2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; + 3CBDE68B2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; + 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; + 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; + 3CCCA5C72ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */; }; + 3CCCA5C82ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */; }; 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3CE11A1229F7BE0900202522 /* DatadogWebViewTracking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3CFD81952ABBB66400977C22 /* MetaTypeExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD81942ABBB66400977C22 /* MetaTypeExtensionsTests.swift */; }; + 3CFD81962ABBB66400977C22 /* MetaTypeExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD81942ABBB66400977C22 /* MetaTypeExtensionsTests.swift */; }; 49274906288048B500ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; 49274907288048B800ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; 49D8C0B72AC5D2160075E427 /* RUM+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */; }; @@ -544,12 +580,8 @@ D2160CC629C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */; }; D2160CC929C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */; }; D2160CCA29C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */; }; - D2160CCB29C0DED100FAA9A5 /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC429C0DED100FAA9A5 /* URLSessionSwizzler.swift */; }; - D2160CCC29C0DED100FAA9A5 /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC429C0DED100FAA9A5 /* URLSessionSwizzler.swift */; }; D2160CD429C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */; }; D2160CD529C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */; }; - D2160CD629C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCE29C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift */; }; - D2160CD729C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCE29C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift */; }; D2160CD829C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */; }; D2160CD929C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */; }; D2160CDC29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */; }; @@ -1809,6 +1841,10 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 3C2206F82AB9DBC600DE780C /* DatadogInternal.framework in Embed Frameworks */, + 3C2206F72AB9DBB600DE780C /* DatadogTrace.framework in Embed Frameworks */, + 3C2206F62AB9DBA700DE780C /* DatadogRUM.framework in Embed Frameworks */, + 3C2206F52AB9DB9000DE780C /* DatadogSessionReplay.framework in Embed Frameworks */, D240687E27CF982D00C04F44 /* DatadogCrashReporting.framework in Embed Frameworks */, D240687C27CF982C00C04F44 /* DatadogCore.framework in Embed Frameworks */, D24C9C4329A7A50D002057CF /* DatadogLogs.framework in Embed Frameworks */, @@ -1828,10 +1864,26 @@ 3C0D5DEB2A54405A00446CF9 /* RUMViewEventsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewEventsFilter.swift; sourceTree = ""; }; 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMocks.swift; sourceTree = ""; }; 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataFormatTests.swift; sourceTree = ""; }; + 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDURLSessionInstrumentationTests+apiTests.m"; sourceTree = ""; }; + 3C2206F22AB9CE9300DE780C /* MetaTypeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaTypeExtensions.swift; sourceTree = ""; }; + 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = ""; }; + 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTracking.swift; sourceTree = ""; }; 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsSanitizerMock.swift; sourceTree = ""; }; + 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzler.swift; sourceTree = ""; }; + 3CB32AD62ACB735600D602ED /* URLSessionSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzlerTests.swift; sourceTree = ""; }; + 3CBDE66D2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskDelegateSwizzler.swift; sourceTree = ""; }; + 3CBDE6702AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzler.swift; sourceTree = ""; }; + 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionInstrumentation.swift; sourceTree = ""; }; + 3CBDE6802AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskDelegateSwizzlerTests.swift; sourceTree = ""; }; + 3CBDE6832AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzlerTests.swift; sourceTree = ""; }; + 3CBDE6862AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTaskDelegate+Tracking.swift"; sourceTree = ""; }; + 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTask+Tracking.swift"; sourceTree = ""; }; + 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionInstrumentation+objc.swift"; sourceTree = ""; }; + 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDURLSessionInstrumentationConfigurationTests.swift; sourceTree = ""; }; 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogWebViewTracking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogWebViewTrackingTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CFD81942ABBB66400977C22 /* MetaTypeExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaTypeExtensionsTests.swift; sourceTree = ""; }; 49274903288048AA00ECD49B /* InternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalProxyTests.swift; sourceTree = ""; }; 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMInternalProxyTests.swift; sourceTree = ""; }; 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUM+Internal.swift"; sourceTree = ""; }; @@ -2401,9 +2453,7 @@ D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationFeature.swift; sourceTree = ""; }; D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskInterception.swift; sourceTree = ""; }; D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogURLSessionDelegate.swift; sourceTree = ""; }; - D2160CC429C0DED100FAA9A5 /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzler.swift; sourceTree = ""; }; D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationFeatureTests.swift; sourceTree = ""; }; - D2160CCE29C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzlerTests.swift; sourceTree = ""; }; D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstPartyHostsTests.swift; sourceTree = ""; }; D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsSanitizerTests.swift; sourceTree = ""; }; D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskInterceptionTests.swift; sourceTree = ""; }; @@ -3772,6 +3822,7 @@ 61133C082423983800786299 /* DatadogObjc */ = { isa = PBXGroup; children = ( + 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */, 61133C092423983800786299 /* Datadog+objc.swift */, 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */, 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */, @@ -3824,6 +3875,7 @@ A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */, A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */, 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */, + 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */, D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */, 61D03BDE273404BB00367DE0 /* RUM */, ); @@ -4606,6 +4658,7 @@ A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */, A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */, D2A434AB2A8E416F0028E329 /* DDSessionReplay+apiTests.m */, + 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */, ); path = ObjcAPITests; sourceTree = ""; @@ -5083,7 +5136,13 @@ D295A16429F299C9007C0E9A /* URLSessionInterceptor.swift */, D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */, D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */, - D2160CC429C0DED100FAA9A5 /* URLSessionSwizzler.swift */, + 3CBDE66D2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift */, + 3CBDE6702AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift */, + 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */, + 3CBDE6862AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift */, + 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */, + 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */, + 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */, ); path = URLSession; sourceTree = ""; @@ -5584,6 +5643,7 @@ D23039DC298D5235001A1FA3 /* DDError.swift */, 61133BBA2423979B00786299 /* SwiftExtensions.swift */, D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */, + 3C2206F22AB9CE9300DE780C /* MetaTypeExtensions.swift */, ); path = Utils; sourceTree = ""; @@ -5593,6 +5653,7 @@ children = ( 613C6B912768FF3100870CBF /* SamplerTests.swift */, 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, + 3CFD81942ABBB66400977C22 /* MetaTypeExtensionsTests.swift */, ); path = Utils; sourceTree = ""; @@ -5697,7 +5758,6 @@ D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */, D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */, D2160CD329C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift */, - D2160CCE29C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift */, D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */, 61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */, 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */, @@ -5705,6 +5765,10 @@ A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */, A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */, A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */, + 3CBDE6802AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift */, + 3CBDE6832AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift */, + 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */, + 3CB32AD62ACB735600D602ED /* URLSessionSwizzlerTests.swift */, ); path = NetworkInstrumentation; sourceTree = ""; @@ -6698,7 +6762,7 @@ }; D23039A4298D513C001A1FA3 = { CreatedOnToolsVersion = 14.2; - LastSwiftMigration = 1420; + LastSwiftMigration = 1410; }; D257953D298ABA65008A1BE5 = { CreatedOnToolsVersion = 14.2; @@ -7427,6 +7491,7 @@ 61A763DC252DB2B3005A23F2 /* NSURLSessionBridge.m in Sources */, D2A1EE442886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */, D29A9FD029DDC58E005C54A4 /* RUMFeatureTests.swift in Sources */, + 3C1890152ABDE9BF00CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */, D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */, 61133C4B2423990D00786299 /* DDLogsTests.swift in Sources */, 614B78ED296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */, @@ -7479,6 +7544,7 @@ D2A1EE3E2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */, 61B5E42926DFB60A000B0A5F /* DDConfiguration+apiTests.m in Sources */, D22743E429DEB933001A7EF9 /* UIViewControllerSwizzlerTests.swift in Sources */, + 3CCCA5C72ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */, 6184751826EFD03400C7C9C5 /* DatadogTestsObserverLoader.m in Sources */, D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */, 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */, @@ -7501,6 +7567,7 @@ 6132BF4224A38D2400D7BD17 /* OTTracer+objc.swift in Sources */, A728ADAB2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */, A79B0F66292BD7CA008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */, + 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, 61133C0E2423983800786299 /* Datadog+objc.swift in Sources */, 61133C102423983800786299 /* Logs+objc.swift in Sources */, 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */, @@ -7854,9 +7921,11 @@ D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */, D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */, + 3CBDE6712AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift in Sources */, D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */, D2EBEE2629BA160F00B15732 /* B3HTTPHeaders.swift in Sources */, D23354FC2A42E32000AFCAE2 /* InternalExtended.swift in Sources */, + 3C2206F32AB9CE9300DE780C /* MetaTypeExtensions.swift in Sources */, D23039F3298D5236001A1FA3 /* DynamicCodingKey.swift in Sources */, D23039FE298D5236001A1FA3 /* FeatureRequestBuilder.swift in Sources */, D2160CE429C0DFEE00FAA9A5 /* MethodSwizzler.swift in Sources */, @@ -7865,6 +7934,7 @@ D2432CF929EDB22C00D93657 /* Flushable.swift in Sources */, D23039F7298D5236001A1FA3 /* AttributesSanitizer.swift in Sources */, D23039EB298D5236001A1FA3 /* DatadogFeature.swift in Sources */, + 3CBDE6742AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */, D23039E4298D5236001A1FA3 /* CarrierInfo.swift in Sources */, D2303A03298D5236001A1FA3 /* DDError.swift in Sources */, D23039F4298D5236001A1FA3 /* AnyCodable.swift in Sources */, @@ -7882,6 +7952,7 @@ D2160C9E29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */, D23039F5298D5236001A1FA3 /* AnyEncodable.swift in Sources */, D2303A00298D5236001A1FA3 /* DatadogExtended.swift in Sources */, + 3CBDE66E2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift in Sources */, D23039E6298D5236001A1FA3 /* Sysctl.swift in Sources */, D2160CF429C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, D23039E1298D5236001A1FA3 /* AppState.swift in Sources */, @@ -7891,11 +7962,13 @@ D2EBEE2329BA160F00B15732 /* B3HTTPHeadersReader.swift in Sources */, D23039F8298D5236001A1FA3 /* InternalLogger.swift in Sources */, D2303A01298D5236001A1FA3 /* DateFormatting.swift in Sources */, + 3CBDE6872AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift in Sources */, D2216EC02A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */, D23039F1298D5236001A1FA3 /* AnyDecodable.swift in Sources */, D2160CC529C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */, D23039DD298D5235001A1FA3 /* DD.swift in Sources */, D2160C9A29C0DE5700FAA9A5 /* FirstPartyHosts.swift in Sources */, + 3CB32AD42ACB733000D602ED /* URLSessionSwizzler.swift in Sources */, D2EBEE2229BA160F00B15732 /* TracePropagationHeadersReader.swift in Sources */, D2303A02298D5236001A1FA3 /* ReadWriteLock.swift in Sources */, D2EBEE2429BA160F00B15732 /* W3CHTTPHeadersReader.swift in Sources */, @@ -7908,11 +7981,11 @@ D23039FB298D5236001A1FA3 /* URLRequestBuilder.swift in Sources */, D23039F6298D5236001A1FA3 /* Attributes.swift in Sources */, D20731CB29A52E6000ECBF94 /* Sampler.swift in Sources */, - D2160CCB29C0DED100FAA9A5 /* URLSessionSwizzler.swift in Sources */, D2EBEE2029BA160F00B15732 /* TracePropagationHeadersWriter.swift in Sources */, D23039F2298D5236001A1FA3 /* AnyDecoder.swift in Sources */, D23039EF298D5236001A1FA3 /* FeatureMessage.swift in Sources */, D2160CA029C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */, + 3C394EF72AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift in Sources */, D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */, D295A16529F299C9007C0E9A /* URLSessionInterceptor.swift in Sources */, D23039E5298D5236001A1FA3 /* DateProvider.swift in Sources */, @@ -7921,6 +7994,7 @@ D23039F0298D5236001A1FA3 /* AnyEncoder.swift in Sources */, D2A783D429A5309F003B03BB /* SwiftExtensions.swift in Sources */, 3C0D5DD72A543B3B00446CF9 /* Event.swift in Sources */, + 3CBDE68A2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */, D22F06D929DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8441,6 +8515,7 @@ files = ( D28F836929C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */, 61B8BA92281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */, + 3C1890162ABDE9C000CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */, 6134CDB22A691E850061CCD9 /* CoreMetricsTests.swift in Sources */, D22743E029DEB8B5001A7EF9 /* VitalCPUReaderTests.swift in Sources */, D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */, @@ -8520,6 +8595,7 @@ D2A1EE452886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */, 61A2CC222A443D330000FF25 /* DDRUMConfigurationTests.swift in Sources */, 6176991C2A86121B0030022B /* HTTPClientMock.swift in Sources */, + 3CCCA5C82ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */, D29294E4291D652D00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */, A7DA18052AB0C91300F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */, A79B0F65292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */, @@ -8590,6 +8666,7 @@ A79B0F67292BD7CC008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */, D2CB6F9E27C5217A00A62B57 /* Datadog+objc.swift in Sources */, D2CB6F9F27C5217A00A62B57 /* Logs+objc.swift in Sources */, + 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, D2CB6FA027C5217A00A62B57 /* Trace+objc.swift in Sources */, D2CB6FA127C5217A00A62B57 /* HTTPHeadersWriter+objc.swift in Sources */, D2CB6FA227C5217A00A62B57 /* DDSpan+objc.swift in Sources */, @@ -8646,9 +8723,11 @@ D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */, D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */, + 3CBDE6722AA08C0B00F6A7B6 /* URLSessionTaskSwizzler.swift in Sources */, D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */, D2EBEE3429BA161100B15732 /* B3HTTPHeaders.swift in Sources */, D23354FD2A42E32000AFCAE2 /* InternalExtended.swift in Sources */, + 3C2206F42AB9CE9300DE780C /* MetaTypeExtensions.swift in Sources */, D2DA235B298D57AA00C6C7E6 /* DynamicCodingKey.swift in Sources */, D2DA235C298D57AA00C6C7E6 /* FeatureRequestBuilder.swift in Sources */, D2160CE629C0DFEE00FAA9A5 /* MethodSwizzler.swift in Sources */, @@ -8657,6 +8736,7 @@ D2432CFA29EDB22C00D93657 /* Flushable.swift in Sources */, D2DA235D298D57AA00C6C7E6 /* AttributesSanitizer.swift in Sources */, D2DA235E298D57AA00C6C7E6 /* DatadogFeature.swift in Sources */, + 3CBDE6752AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */, D2DA235F298D57AA00C6C7E6 /* CarrierInfo.swift in Sources */, D2DA2360298D57AA00C6C7E6 /* DDError.swift in Sources */, D2DA2361298D57AA00C6C7E6 /* AnyCodable.swift in Sources */, @@ -8674,6 +8754,7 @@ D2160C9F29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */, D2DA2369298D57AA00C6C7E6 /* AnyEncodable.swift in Sources */, D2DA236A298D57AA00C6C7E6 /* DatadogExtended.swift in Sources */, + 3CBDE66F2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift in Sources */, D2DA236B298D57AA00C6C7E6 /* Sysctl.swift in Sources */, D2160CF529C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, D2DA236C298D57AA00C6C7E6 /* AppState.swift in Sources */, @@ -8683,11 +8764,13 @@ D2EBEE3129BA161100B15732 /* B3HTTPHeadersReader.swift in Sources */, D2DA236E298D57AA00C6C7E6 /* InternalLogger.swift in Sources */, D2DA236F298D57AA00C6C7E6 /* DateFormatting.swift in Sources */, + 3CBDE6882AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift in Sources */, D2216EC12A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */, D2DA2370298D57AA00C6C7E6 /* AnyDecodable.swift in Sources */, D2160CC629C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */, D2DA2372298D57AA00C6C7E6 /* DD.swift in Sources */, D2160C9B29C0DE5700FAA9A5 /* FirstPartyHosts.swift in Sources */, + 3CB32AD52ACB733000D602ED /* URLSessionSwizzler.swift in Sources */, D2EBEE3029BA161100B15732 /* TracePropagationHeadersReader.swift in Sources */, D2DA2373298D57AA00C6C7E6 /* ReadWriteLock.swift in Sources */, D2EBEE3229BA161100B15732 /* W3CHTTPHeadersReader.swift in Sources */, @@ -8700,11 +8783,11 @@ D2DA2377298D57AA00C6C7E6 /* URLRequestBuilder.swift in Sources */, D2DA2378298D57AA00C6C7E6 /* Attributes.swift in Sources */, D20731CC29A52E6000ECBF94 /* Sampler.swift in Sources */, - D2160CCC29C0DED100FAA9A5 /* URLSessionSwizzler.swift in Sources */, D2EBEE2E29BA161100B15732 /* TracePropagationHeadersWriter.swift in Sources */, D2DA2379298D57AA00C6C7E6 /* AnyDecoder.swift in Sources */, D2DA237A298D57AA00C6C7E6 /* FeatureMessage.swift in Sources */, D2160CA129C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */, + 3C394EF82AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift in Sources */, D22F06D829DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */, D295A16629F299C9007C0E9A /* URLSessionInterceptor.swift in Sources */, D2DA237B298D57AA00C6C7E6 /* DateProvider.swift in Sources */, @@ -8713,6 +8796,7 @@ D2DA237E298D57AA00C6C7E6 /* AnyEncoder.swift in Sources */, D2A783D529A530A0003B03BB /* SwiftExtensions.swift in Sources */, 3C0D5DD82A543B3B00446CF9 /* Event.swift in Sources */, + 3CBDE68B2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */, D22F06DA29DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8721,18 +8805,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D2160CD629C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift in Sources */, + 3C394EFA2AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, D26416B62A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */, D2EBEE3C29BA163E00B15732 /* B3HTTPHeadersWriterTests.swift in Sources */, + 3CBDE6812AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */, D21AE6BC29E5EDAF0064BF29 /* TelemetryTests.swift in Sources */, D2DA23A3298D58F400C6C7E6 /* AnyEncodableTests.swift in Sources */, D263BCB429DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */, 3C0D5DF52A5443B100446CF9 /* DataFormatTests.swift in Sources */, D2EBEE4429BA168200B15732 /* TraceIDTests.swift in Sources */, + 3CBDE6842AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift in Sources */, D2EBEE4329BA168200B15732 /* TraceIDGeneratorTests.swift in Sources */, D2DA23A7298D58F400C6C7E6 /* AppStateHistoryTests.swift in Sources */, D2DA23A5298D58F400C6C7E6 /* AnyDecodableTests.swift in Sources */, + 3CFD81952ABBB66400977C22 /* MetaTypeExtensionsTests.swift in Sources */, D2EBEE3D29BA163E00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */, + 3CB32AD72ACB735600D602ED /* URLSessionSwizzlerTests.swift in Sources */, D2DA23A4298D58F400C6C7E6 /* AnyCodableTests.swift in Sources */, D2160CDE29C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift in Sources */, D2DA23A1298D58F400C6C7E6 /* ReadWriteLockTests.swift in Sources */, @@ -8760,18 +8848,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D2160CD729C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift in Sources */, + 3C394EFB2AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, D26416B72A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */, D2EBEE4029BA163F00B15732 /* B3HTTPHeadersWriterTests.swift in Sources */, + 3CBDE6822AA092A200F6A7B6 /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */, D21AE6BD29E5EDAF0064BF29 /* TelemetryTests.swift in Sources */, D2DA23B1298D59DC00C6C7E6 /* AnyEncodableTests.swift in Sources */, D263BCB529DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */, 3C0D5DF62A5443B100446CF9 /* DataFormatTests.swift in Sources */, D2EBEE4629BA168400B15732 /* TraceIDTests.swift in Sources */, + 3CBDE6852AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift in Sources */, D2EBEE4529BA168400B15732 /* TraceIDGeneratorTests.swift in Sources */, D2DA23B2298D59DC00C6C7E6 /* AppStateHistoryTests.swift in Sources */, D2DA23B3298D59DC00C6C7E6 /* AnyDecodableTests.swift in Sources */, + 3CFD81962ABBB66400977C22 /* MetaTypeExtensionsTests.swift in Sources */, D2EBEE4129BA163F00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */, + 3CB32AD82ACB735600D602ED /* URLSessionSwizzlerTests.swift in Sources */, D2DA23B4298D59DC00C6C7E6 /* AnyCodableTests.swift in Sources */, D2160CDF29C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift in Sources */, D2DA23B5298D59DC00C6C7E6 /* ReadWriteLockTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index c6f9fa7730..db28c5f907 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -106,7 +106,7 @@ @import DatadogObjc; +@import DatadogTrace; @interface DDNSURLSessionDelegate_apiTests : XCTestCase @end @@ -18,6 +19,27 @@ @implementation DDNSURLSessionDelegate_apiTests #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-value" +- (void)setUp { + [super setUp]; + + DDConfiguration *configuration = [[DDConfiguration alloc] initWithClientToken:@"abc" env:@"def"]; + [DDDatadog initializeWithConfiguration:configuration trackingConsent:[DDTrackingConsent notGranted]]; + + DDTraceConfiguration *config = [[DDTraceConfiguration alloc] init]; + DDTraceFirstPartyHostsTracing *tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHosts:[NSSet new] sampleRate:20]; + DDTraceURLSessionTracking *urlSessionTracking = [[DDTraceURLSessionTracking alloc] initWithFirstPartyHostsTracing:tracing]; + [config setURLSessionTracking:urlSessionTracking]; + [DDTrace enableWith:config]; +} + +- (void)tearDown { + [super tearDown]; + + [DDURLSessionInstrumentation disableWithDelegateClass:[DDNSURLSessionDelegate class]]; + [DDDatadog clearAllData]; + [DDDatadog flushAndDeinitialize]; +} + - (void)testDDNSURLSessionDelegateAPI { [[DDNSURLSessionDelegate alloc] init]; [[DDNSURLSessionDelegate alloc] initWithAdditionalFirstPartyHosts:[NSSet setWithArray:@[]]]; diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDURLSessionInstrumentationTests+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDURLSessionInstrumentationTests+apiTests.m new file mode 100644 index 0000000000..84f909d021 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDURLSessionInstrumentationTests+apiTests.m @@ -0,0 +1,68 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +#import +#include +@import DatadogObjc; +@import DatadogTrace; + +#import + +@interface MockDelegate : NSObject +@end + +@implementation MockDelegate +@end + +@interface DDURLSessionInstrumentationTests_apiTests : XCTestCase +@end + +@implementation DDURLSessionInstrumentationTests_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)setUp { + [super setUp]; + + DDConfiguration *configuration = [[DDConfiguration alloc] initWithClientToken:@"abc" env:@"def"]; + [DDDatadog initializeWithConfiguration:configuration trackingConsent:[DDTrackingConsent notGranted]]; + + DDTraceConfiguration *config = [[DDTraceConfiguration alloc] init]; + DDTraceFirstPartyHostsTracing *tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHosts:[NSSet new] sampleRate:20]; + DDTraceURLSessionTracking *urlSessionTracking = [[DDTraceURLSessionTracking alloc] initWithFirstPartyHostsTracing:tracing]; + [config setURLSessionTracking:urlSessionTracking]; + [DDTrace enableWith:config]; +} + +- (void)tearDown { + [super tearDown]; + + [DDDatadog clearAllData]; + [DDDatadog flushAndDeinitialize]; +} + +- (void)testWorkflow { + XCTestExpectation *expectation = [self expectationWithDescription:@"task completed"]; + DDURLSessionInstrumentationConfiguration *config = [[DDURLSessionInstrumentationConfiguration alloc] initWithDelegateClass:[MockDelegate class]]; + [DDURLSessionInstrumentation enableWithConfiguration:config]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:[MockDelegate new] delegateQueue:nil]; + NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://status.datadoghq.com"] + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + [expectation fulfill]; + }]; + [task resume]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [DDURLSessionInstrumentation disableWithDelegateClass:[MockDelegate class]]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift b/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift index bf4f089cc5..dec7193873 100644 --- a/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift +++ b/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift @@ -33,22 +33,13 @@ internal class DatadogTestsObserver: NSObject, XCTestObservation { """ ), .init( - assert: { activeSwizzlingNames.isEmpty }, + assert: { Swizzling.activeSwizzlingNames.isEmpty }, problem: "No swizzling must be applied.", solution: """ Make sure all applied swizzling are reset by the end of test with `unswizzle()`. - `DatadogTestsObserver` found \(activeSwizzlingNames.count) leaked swizzlings: - \(activeSwizzlingNames.joined(separator: ", ")) - """ - ), - .init( - assert: { URLSessionSwizzler.bindingsCount == 0 }, - problem: "No `URLSessionSwizzler` must be bonded.", - solution: """ - Make sure all applied `URLSessionSwizzler.bind()` are reset by the end of test with `URLSessionSwizzler.unbind()`. - - `DatadogTestsObserver` found \(URLSessionSwizzler.bindingsCount) bindings left. + `DatadogTestsObserver` found \(Swizzling.activeSwizzlingNames.count) leaked swizzlings: + \(Swizzling.activeSwizzlingNames.joined(separator: ", ")) """ ), .init( @@ -141,6 +132,34 @@ internal class DatadogTestsObserver: NSObject, XCTestObservation { If all above conditions are met, this failure might indicate a memory leak in the implementation. """ + ), + .init( + assert: { URLSessionTaskDelegateSwizzler.isBinded == false }, + problem: "No URLSessionTaskDelegate swizzling must be applied.", + solution: """ + Make sure all the binded delegates are unbinded by the end of test with `URLSessionTaskDelegateSwizzler.unbind(delegate:)`. + """ + ), + .init( + assert: { URLSessionDataDelegateSwizzler.isBinded == false }, + problem: "No URLSessionDataDelegate swizzling must be applied.", + solution: """ + Make sure all the binded delegates are unbinded by the end of test with `URLSessionDataDelegateSwizzler.unbind(delegate:)`. + """ + ), + .init( + assert: { URLSessionTaskSwizzler.isBinded == false }, + problem: "No URLSessionTask swizzling must be applied.", + solution: """ + Make sure all the binded delegates are unbinded by the end of test with `URLSessionTaskSwizzler.unbind()`. + """ + ), + .init( + assert: { URLSessionSwizzler.isBinded == false }, + problem: "No URLSession swizzling must be applied.", + solution: """ + Make sure all the binded delegates are unbinded by the end of test with `URLSessionSwizzler.unbind()`. + """ ) ] diff --git a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift index 84eb974b2f..1464b800fb 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift @@ -46,7 +46,7 @@ extension DatadogCoreProtocol { /// /// - Parameter urlSessionHandler: The `URLSession` handlers to register. public func register(urlSessionHandler: DatadogURLSessionHandler) throws { - let feature = try get(feature: NetworkInstrumentationFeature.self) ?? .init() + let feature = get(feature: NetworkInstrumentationFeature.self) ?? .init() feature.handlers.append(urlSessionHandler) try register(feature: feature) } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift b/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift index 075c393879..bd217e58f8 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift @@ -8,7 +8,7 @@ import Foundation /// A struct that represents a dictionary of host names and tracing header types. public struct FirstPartyHosts: Equatable { - fileprivate var hostsWithTracingHeaderTypes: [String: Set] + internal var hostsWithTracingHeaderTypes: [String: Set] public var hosts: Set { return Set(hostsWithTracingHeaderTypes.keys) @@ -37,6 +37,17 @@ public struct FirstPartyHosts: Equatable { self.init(hostsWithTracingHeaderTypes: [:]) } + internal init?(firstPartyHosts: URLSessionInstrumentation.FirstPartyHostsTracing?) { + switch firstPartyHosts { + case .trace(let hosts): + self.init(hosts) + case .traceWithHeaders(let hostsWithHeaders): + self.init(hostsWithTracingHeaderTypes: hostsWithHeaders) + default: + return nil + } + } + internal init( hostsWithTracingHeaderTypes: [String: Set], hostsSanitizer: HostsSanitizing = HostsSanitizer() diff --git a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift index 5309d351fc..a410e3e653 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift @@ -43,11 +43,95 @@ internal final class NetworkInstrumentationFeature: DatadogFeature { /// The interceptions **must** be accessed using the `queue`. private var interceptions: [URLSessionTask: URLSessionTaskInterception] = [:] - init() throws { - try URLSessionSwizzler.bind() + /// Swizzles `URLSessionTaskDelegate`, `URLSessionDataDelegate`, and `URLSessionTask` methods + /// to intercept `URLSessionTask` lifecycles. + /// + /// - Parameter configuration: The configuration to use for swizzling. + /// Note: We are only concerned with type of the delegate here but to provide compile time safety, we + /// use the instance of the delegate to get the type. + internal func bindIfNeeded(configuration: URLSessionInstrumentation.Configuration) throws { + let configuredFirstPartyHosts = FirstPartyHosts(firstPartyHosts: configuration.firstPartyHostsTracing) ?? .init() + + try URLSessionTaskDelegateSwizzler.bindIfNeeded( + delegateClass: configuration.delegateClass, + interceptDidFinishCollecting: { [weak self] session, task, metrics in + self?.queue.async { + self?._task(task, didFinishCollecting: metrics) + session.delegate?.interceptor?.task(task, didFinishCollecting: metrics) + + // iOS 16 and above, didCompleteWithError is not called hence we use task state to detect task completion + // while prior to iOS 15, task state doesn't change to completed hence we use didCompleteWithError to detect task completion + if #available(iOS 15, tvOS 15, *) { + self?._task(task, didCompleteWithError: task.error) + session.delegate?.interceptor?.task(task, didCompleteWithError: task.error) + } + } + }, interceptDidCompleteWithError: { [weak self] session, task, error in + self?.queue.async { + // prior to iOS 15, task state doesn't change to completed + // hence we use didCompleteWithError to detect task completion + self?._task(task, didCompleteWithError: task.error) + session.delegate?.interceptor?.task(task, didCompleteWithError: task.error) + } + } + ) + + try URLSessionDataDelegateSwizzler.bindIfNeeded(delegateClass: configuration.delegateClass, interceptDidReceive: { [weak self] session, task, data in + // sync update to task prevents a race condition where the currentRequest could already be sent to the transport + self?.queue.sync { + self?._task(task, didReceive: data) + session.delegate?.interceptor?.task(task, didReceive: data) + } + }) + + if #available(iOS 13, tvOS 13, *) { + try URLSessionTaskSwizzler.bindIfNeeded(interceptResume: { [weak self] task in + self?.queue.sync { + let additionalFirstPartyHosts = configuredFirstPartyHosts + task.firstPartyHosts + self?._intercept(task: task, additionalFirstPartyHosts: additionalFirstPartyHosts) + } + }) + } else { + try URLSessionSwizzler.bindIfNeeded(interceptURLRequest: { request in + return self.intercept(request: request, additionalFirstPartyHosts: configuredFirstPartyHosts) + }, interceptTask: { [weak self] task in + self?.queue.async { + let additionalFirstPartyHosts = configuredFirstPartyHosts + task.firstPartyHosts + self?._intercept(task: task, additionalFirstPartyHosts: additionalFirstPartyHosts) + } + }) + } + } + + private func firstPartyHosts(configuration: URLSessionInstrumentation.Configuration, delegate: URLSessionDelegate) -> FirstPartyHosts? { + var firstPartyHosts = FirstPartyHosts(firstPartyHosts: configuration.firstPartyHostsTracing) + + if let datadogDelegate = delegate as? DatadogURLSessionDelegate { + firstPartyHosts += datadogDelegate.firstPartyHosts + } + + return firstPartyHosts + } + + internal func unbindAll() { + URLSessionTaskDelegateSwizzler.unbindAll() + URLSessionDataDelegateSwizzler.unbindAll() + URLSessionTaskSwizzler.unbind() + URLSessionSwizzler.unbind() } - deinit { + /// Unswizzles `URLSessionTaskDelegate`, `URLSessionDataDelegate`, `URLSessionTask` and `URLSession` methods + /// - Parameter delegateClass: The delegate class to unswizzle. + internal func unbind(delegateClass: URLSessionDataDelegate.Type) { + URLSessionTaskDelegateSwizzler.unbind(delegateClass: delegateClass) + URLSessionDataDelegateSwizzler.unbind(delegateClass: delegateClass) + + guard URLSessionTaskDelegateSwizzler.didFinishCollectingMap.isEmpty, + URLSessionDataDelegateSwizzler.didReceiveMap.isEmpty else { + return + } + + URLSessionTaskSwizzler.unbind() URLSessionSwizzler.unbind() } } @@ -78,29 +162,47 @@ extension NetworkInstrumentationFeature { /// - task: The created task. /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts?) { - guard let request = task.originalRequest else { + // sync update to task prevents a race condition where the currentRequest could already be sent to the transport + queue.sync { [weak self] in + self?._intercept(task: task, additionalFirstPartyHosts: additionalFirstPartyHosts) + } + } + + private func _intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts?) { + guard let originalRequest = task.originalRequest else { return } - queue.async { - let firstPartyHosts = self.firstPartyHosts(with: additionalFirstPartyHosts) + var interceptedRequest: URLRequest + /// task.setValue is not available on iOS 12, hence for iOS 12 we modify the request by swizzling URLSession methods + if #available(iOS 13, tvOS 13, *) { + let request = self.intercept(request: originalRequest, additionalFirstPartyHosts: additionalFirstPartyHosts) + interceptedRequest = request + task.setValue(interceptedRequest, forKey: "currentRequest") + } else { + interceptedRequest = originalRequest + } + + let firstPartyHosts = self.firstPartyHosts(with: additionalFirstPartyHosts) - let interception = URLSessionTaskInterception( - request: request, - isFirstParty: firstPartyHosts.isFirstParty(url: request.url) + let interception = self.interceptions[task] ?? + URLSessionTaskInterception( + request: interceptedRequest, + isFirstParty: firstPartyHosts.isFirstParty(url: interceptedRequest.url) ) - if let trace = self.extractTrace(firstPartyHosts: firstPartyHosts, request: request) { - interception.register(traceID: trace.traceID, spanID: trace.spanID, parentSpanID: trace.parentSpanID) - } + interception.register(request: interceptedRequest) - if let origin = request.value(forHTTPHeaderField: TracingHTTPHeaders.originField) { - interception.register(origin: origin) - } + if let trace = self.extractTrace(firstPartyHosts: firstPartyHosts, request: interceptedRequest) { + interception.register(traceID: trace.traceID, spanID: trace.spanID, parentSpanID: trace.parentSpanID) + } - self.interceptions[task] = interception - self.handlers.forEach { $0.interceptionDidStart(interception: interception) } + if let origin = interceptedRequest.value(forHTTPHeaderField: TracingHTTPHeaders.originField) { + interception.register(origin: origin) } + + self.interceptions[task] = interception + self.handlers.forEach { $0.interceptionDidStart(interception: interception) } } /// Tells the interceptors that metrics were collected for the given task. @@ -109,18 +211,22 @@ extension NetworkInstrumentationFeature { /// - task: The task whose metrics have been collected. /// - metrics: The collected metrics. func task(_ task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - queue.async { - guard let interception = self.interceptions[task] else { - return - } + queue.async { [weak self] in + self?._task(task, didFinishCollecting: metrics) + } + } - interception.register( - metrics: ResourceMetrics(taskMetrics: metrics) - ) + private func _task(_ task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + guard let interception = self.interceptions[task] else { + return + } - if interception.isDone { - self.finish(task: task, interception: interception) - } + interception.register( + metrics: ResourceMetrics(taskMetrics: metrics) + ) + + if interception.isDone { + self.finish(task: task, interception: interception) } } @@ -130,7 +236,16 @@ extension NetworkInstrumentationFeature { /// - task: The task that provided data. /// - data: A data object containing the transferred data. func task(_ task: URLSessionTask, didReceive data: Data) { - queue.async { self.interceptions[task]?.register(nextData: data) } + queue.async { [weak self] in + self?._task(task, didReceive: data) + } + } + + private func _task(_ task: URLSessionTask, didReceive data: Data) { + guard let interception = self.interceptions[task] else { + return + } + interception.register(nextData: data) } /// Tells the interceptors that the task did complete. @@ -139,19 +254,23 @@ extension NetworkInstrumentationFeature { /// - task: The task that has finished transferring data. /// - error: If an error occurred, an error object indicating how the transfer failed, otherwise NULL. func task(_ task: URLSessionTask, didCompleteWithError error: Error?) { - queue.async { - guard let interception = self.interceptions[task] else { - return - } + queue.async { [weak self] in + self?._task(task, didCompleteWithError: error) + } + } - interception.register( - response: task.response, - error: error - ) + private func _task(_ task: URLSessionTask, didCompleteWithError error: Error?) { + guard let interception = self.interceptions[task] else { + return + } - if interception.isDone { - self.finish(task: task, interception: interception) - } + interception.register( + response: task.response, + error: error + ) + + if interception.isDone { + self.finish(task: task, interception: interception) } } @@ -160,8 +279,8 @@ extension NetworkInstrumentationFeature { } private func finish(task: URLSessionTask, interception: URLSessionTaskInterception) { - interceptions[task] = nil handlers.forEach { $0.interceptionDidComplete(interception: interception) } + interceptions[task] = nil } private func extractTrace(firstPartyHosts: FirstPartyHosts, request: URLRequest) -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift index 0484489502..17381ccd9b 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift @@ -12,12 +12,6 @@ public typealias DDURLSessionDelegate = DatadogURLSessionDelegate /// The implementation must ensure that required methods are called on the `ddURLSessionDelegate`. @objc public protocol __URLSessionDelegateProviding: URLSessionDelegate { - /// Datadog delegate object. - /// The class implementing `DDURLSessionDelegateProviding` must ensure that following method calls are forwarded to `ddURLSessionDelegate`: - /// - `func urlSession(_:task:didFinishCollecting:)` - /// - `func urlSession(_:task:didCompleteWithError:)` - /// - `func urlSession(_:dataTask:didReceive:)` - var ddURLSessionDelegate: DatadogURLSessionDelegate { get } } /// The `URLSession` delegate object which enables network requests instrumentation. **It must be @@ -34,7 +28,7 @@ open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate { /* private */ public let firstPartyHosts: FirstPartyHosts /// The instance of the SDK core notified by this delegate. - /// + /// /// It must be a weak reference, because `URLSessionDelegate` can last longer than core instance. /// Any `URLSession` will retain its delegate until `.invalidateAndCancel()` is called. private weak var core: DatadogCoreProtocol? @@ -43,6 +37,15 @@ open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate { override public init() { core = nil firstPartyHosts = .init() + + URLSessionInstrumentation.enable( + with: .init( + delegateClass: DatadogURLSessionDelegate.self, + firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: firstPartyHosts.hostsWithTracingHeaderTypes) + ), + in: core ?? CoreRegistry.default + ) + super.init() } @@ -89,6 +92,14 @@ open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate { ) { self.core = core self.firstPartyHosts = FirstPartyHosts(additionalFirstPartyHostsWithHeaderTypes) + + URLSessionInstrumentation.enable( + with: .init( + delegateClass: DatadogURLSessionDelegate.self, + firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: firstPartyHosts.hostsWithTracingHeaderTypes) + ), + in: core ?? CoreRegistry.default + ) super.init() } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionDataDelegateSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionDataDelegateSwizzler.swift new file mode 100644 index 0000000000..62cbcef6b7 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionDataDelegateSwizzler.swift @@ -0,0 +1,126 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// Swizzles `URLSessionDataDelegate` callbacks. +internal class URLSessionDataDelegateSwizzler { + private static var _didReceiveMap: [String: DidReceive?] = [:] + static var didReceiveMap: [String: DidReceive?] { + get { + lock.lock() + defer { lock.unlock() } + return _didReceiveMap + } + set { + lock.lock() + defer { lock.unlock() } + _didReceiveMap = newValue + } + } + + private static var lock = NSRecursiveLock() + + static var isBinded: Bool { + lock.lock() + defer { lock.unlock() } + return didReceiveMap.isEmpty == false + } + + static func bindIfNeeded( + delegateClass: URLSessionDataDelegate.Type, + interceptDidReceive: @escaping (URLSession, URLSessionDataTask, Data) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + + guard isBinded == false else { + return + } + + try bind(delegateClass: delegateClass, interceptDidReceive: interceptDidReceive) + } + + static func bind( + delegateClass: URLSessionDataDelegate.Type, + interceptDidReceive: @escaping (URLSession, URLSessionDataTask, Data + ) -> Void) throws { + lock.lock() + defer { lock.unlock() } + + let didReceive = try DidReceive.build(klass: delegateClass) + let key = MetaTypeExtensions.key(from: delegateClass) + + didReceive.swizzle(intercept: interceptDidReceive) + didReceiveMap[key] = didReceive + } + + static func unbind(delegateClass: URLSessionDataDelegate.Type) { + lock.lock() + defer { lock.unlock() } + + let key = MetaTypeExtensions.key(from: delegateClass) + didReceiveMap[key]??.unswizzle() + didReceiveMap.removeValue(forKey: key) + } + + static func unbindAll() { + lock.lock() + defer { lock.unlock() } + + didReceiveMap.forEach { _, didReceive in + didReceive?.unswizzle() + } + didReceiveMap.removeAll() + } + + /// Swizzles `urlSession(_:dataTask:didReceive:)` callback. + /// This callback is called when the response is received. + /// It is called multiple times for a single request, each time with a new chunk of data. + class DidReceive: MethodSwizzler<@convention(c) (URLSessionDataDelegate, Selector, URLSession, URLSessionDataTask, Data) -> Void, @convention(block) (URLSessionDataDelegate, URLSession, URLSessionDataTask, Data) -> Void> { + private static let selector = #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:)) + + private let method: FoundMethod + + static func build(klass: URLSessionDataDelegate.Type) throws -> DidReceive { + return try DidReceive(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + do { + method = try Self.findMethod(with: selector, in: klass) + } catch { + // URLSessionDataDelegate doesn't implement the selector, so we inject it and swizzle it + let block: @convention(block) (URLSessionDataDelegate, URLSession, URLSessionDataTask, Data) -> Void = { delegate, session, task, data in + } + let imp = imp_implementationWithBlock(block) + /* + v@:@@@ means: + v - return type is void + @ - self + : - selector + @ - first argument is an object + @ - second argument is an object + @ - third argument is an object + */ + class_addMethod(klass, selector, imp, "v@:@@@") + method = try Self.findMethod(with: selector, in: klass) + } + + super.init() + } + + func swizzle(intercept: @escaping (URLSession, URLSessionDataTask, Data) -> Void) { + typealias Signature = @convention(block) (URLSessionDataDelegate, URLSession, URLSessionDataTask, Data) -> Void + swizzle(method) { previousImplementation -> Signature in + return { delegate, session, task, data in + intercept(session, task, data) + return previousImplementation(delegate, Self.selector, session, task, data) + } + } + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift new file mode 100644 index 0000000000..45da9170d3 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift @@ -0,0 +1,85 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// An entry point to enable URLSession instrumentation. +public enum URLSessionInstrumentation { + /// Enables URLSession instrumentation. + /// + /// - Parameters: + /// - configuration: Configuration of the feature. + /// - core: The instance of Datadog SDK to enable URLSession instrumentation in (global instance by default). + public static func enable(with configuration: URLSessionInstrumentation.Configuration, in core: DatadogCoreProtocol = CoreRegistry.default) { + do { + try enableOrThrow(with: configuration, in: core) + } catch let error { + consolePrint("\(error)") + } + } + + internal static func enableOrThrow(with configuration: URLSessionInstrumentation.Configuration, in core: DatadogCoreProtocol) throws { + guard let feature = core.get(feature: NetworkInstrumentationFeature.self) else { + throw ProgrammerError(description: "URLSession tracking must be enabled before enabling URLSessionInstrumentation using either RUM or Trace feature.") + } + + try feature.bindIfNeeded(configuration: configuration) + } + + /// Disables URLSession instrumentation. + /// - Parameters: + /// - delegateClass: The delegate class to unbind. + /// - core: The instance of Datadog SDK to disable URLSession instrumentation in (global instance by default). + public static func disable(delegateClass: URLSessionDataDelegate.Type, in core: DatadogCoreProtocol = CoreRegistry.default) { + do { + try disableOrThrow(delegateClass: delegateClass, in: core) + } catch let error { + consolePrint("\(error)") + } + } + + internal static func disableOrThrow(delegateClass: URLSessionDataDelegate.Type, in core: DatadogCoreProtocol) throws { + guard let feature = core.get(feature: NetworkInstrumentationFeature.self) else { + throw ProgrammerError(description: "URLSession tracking must be enabled before enabling URLSessionInstrumentation using either RUM or Trace feature.") + } + + feature.unbind(delegateClass: delegateClass) + } +} + +extension URLSessionInstrumentation { + /// Configuration of URLSession instrumentation. + public struct Configuration { + /// The delegate class to be used to swizzle URLSessionTaskDelegate & URLSessionDataDelegate methods. + public var delegateClass: URLSessionDataDelegate.Type + + /// Additional first party hosts to consider in the interception. + public var firstPartyHostsTracing: FirstPartyHostsTracing? + + /// Configuration of URLSession instrumentation. + /// - Parameters: + /// - delegate: The delegate class to be used to swizzle URLSessionTaskDelegate & URLSessionDataDelegate methods. + /// - firstPartyHostsTracing: Additional first party hosts to consider in the interception. + public init(delegateClass: URLSessionDataDelegate.Type, firstPartyHostsTracing: FirstPartyHostsTracing? = nil) { + self.delegateClass = delegateClass + self.firstPartyHostsTracing = firstPartyHostsTracing + } + } + + /// Defines configuration for first-party hosts in distributed tracing. + public enum FirstPartyHostsTracing { + /// Trace the specified hosts using Datadog tracing headers. + /// + /// - Parameters: + /// - hosts: The set of hosts to inject tracing headers. Note: Hosts must not include the "http(s)://" prefix. + case trace(hosts: Set) + + /// Trace given hosts with using custom tracing headers. + /// + /// - `hostsWithHeaders` - Dictionary of hosts and tracing header types to use. Note: Hosts must not include "http(s)://" prefix. + case traceWithHeaders(hostsWithHeaders: [String: Set]) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift index 34d8a413d2..14af26915f 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift @@ -6,84 +6,66 @@ import Foundation +/// Swizzles `URLSession` methods. internal class URLSessionSwizzler { - /// Counts of bindings of the URL swizzler. - /// - /// This value will increment for each call to the `bind()` method. - /// Calling `unbind()` will decrement the count, when reaching zero, the swizzler is disabled. - internal private(set) static var bindingsCount: UInt = 0 - /// The binding lock. - private static var lock = NSLock() - /// `URLSession.dataTask(with:completionHandler:)` (for `URLRequest`) swizzling. - internal private(set) static var dataTaskWithURLRequestAndCompletion: DataTaskWithURLRequestAndCompletion? - /// `URLSession.dataTask(with:)` (for `URLRequest`) swizzling. - internal private(set) static var dataTaskWithURLRequest: DataTaskWithURLRequest? - /// `URLSession.dataTask(with:completionHandler:)` (for `URL`) swizzling. Only applied on iOS 13 and above. - internal private(set) static var dataTaskWithURLAndCompletion: DataTaskWithURLAndCompletion? - /// `URLSession.dataTask(with:)` (for `URL`) swizzling. Only applied on iOS 13 and above. - internal private(set) static var dataTaskWithURL: DataTaskWithURL? + private static var _dataTaskWithURLRequestAndCompletion: DataTaskWithURLRequestAndCompletion? + static var dataTaskWithURLRequestAndCompletion: DataTaskWithURLRequestAndCompletion? { + get { + lock.lock() + defer { lock.unlock() } + return _dataTaskWithURLRequestAndCompletion + } + set { + lock.lock() + defer { lock.unlock() } + _dataTaskWithURLRequestAndCompletion = newValue + } + } + + private static var lock = NSRecursiveLock() - static func bind() throws { + static var isBinded: Bool { lock.lock() defer { lock.unlock() } + return dataTaskWithURLRequestAndCompletion != nil + } - guard bindingsCount == 0 else { - return bindingsCount += 1 - } + static func bindIfNeeded( + interceptURLRequest: @escaping (URLRequest) -> URLRequest?, + interceptTask: @escaping (URLSessionTask) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } - if #available(iOS 13.0, *) { - // Prior to iOS 13.0 we do not apply following swizzlings, as those methods call - // the `URLSession.dataTask(with:completionHandler:)` internally which is managed - // by the `DataTaskWithURLRequestAndCompletion` swizzling. - dataTaskWithURLAndCompletion = try DataTaskWithURLAndCompletion.build() - dataTaskWithURL = try DataTaskWithURL.build() + guard dataTaskWithURLRequestAndCompletion == nil else { + return } - dataTaskWithURLRequestAndCompletion = try DataTaskWithURLRequestAndCompletion.build() - dataTaskWithURLRequest = try DataTaskWithURLRequest.build() + try bind(interceptURLRequest: interceptURLRequest, interceptTask: interceptTask) + } - dataTaskWithURLRequestAndCompletion?.swizzle() - dataTaskWithURLAndCompletion?.swizzle() - dataTaskWithURLRequest?.swizzle() - dataTaskWithURL?.swizzle() + static func bind( + interceptURLRequest: @escaping (URLRequest) -> URLRequest?, + interceptTask: @escaping (URLSessionTask) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } - bindingsCount += 1 + self.dataTaskWithURLRequestAndCompletion = try DataTaskWithURLRequestAndCompletion.build() + dataTaskWithURLRequestAndCompletion?.swizzle(interceptRequest: interceptURLRequest, interceptTask: interceptTask) } static func unbind() { lock.lock() defer { lock.unlock() } - - if bindingsCount == 0 { - return - } - - if bindingsCount > 1 { - return bindingsCount -= 1 - } - dataTaskWithURLRequestAndCompletion?.unswizzle() - dataTaskWithURLRequest?.unswizzle() - dataTaskWithURLAndCompletion?.unswizzle() - dataTaskWithURL?.unswizzle() - dataTaskWithURLRequestAndCompletion = nil - dataTaskWithURLRequest = nil - dataTaskWithURLAndCompletion = nil - dataTaskWithURL = nil - - bindingsCount -= 1 } - // MARK: - Swizzlings - typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void - /// Swizzles the `URLSession.dataTask(with:completionHandler:)` for `URLRequest`. - class DataTaskWithURLRequestAndCompletion: MethodSwizzler< - @convention(c) (URLSession, Selector, URLRequest, CompletionHandler?) -> URLSessionDataTask, - @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask - > { + /// Swizzles `URLSession.dataTask(with:completionHandler:)` method. + class DataTaskWithURLRequestAndCompletion: MethodSwizzler<@convention(c) (URLSession, Selector, URLRequest, CompletionHandler?) -> URLSessionDataTask, @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask> { private static let selector = #selector( URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URLRequest, @escaping CompletionHandler) -> URLSessionDataTask ) @@ -102,181 +84,16 @@ internal class URLSessionSwizzler { super.init() } - func swizzle() { + func swizzle( + interceptRequest: @escaping (URLRequest) -> URLRequest?, + interceptTask: @escaping (URLSessionTask) -> Void + ) { typealias Signature = @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask swizzle(method) { previousImplementation -> Signature in return { session, request, completionHandler -> URLSessionDataTask in - guard - let delegate = (session.delegate as? __URLSessionDelegateProviding)?.ddURLSessionDelegate, - let interceptor = delegate.interceptor - else { - return previousImplementation(session, Self.selector, request, completionHandler) - } - - guard let completionHandler = completionHandler else { - // The `completionHandler` can be `nil` in two cases: - // - on iOS 11 or 12, where `dataTask(with:)` (for `URL` and `URLRequest`) calls - // the `dataTask(with:completionHandler:)` (for `URLRequest`) internally by nullifying the completion block. - // - when `[session dataTaskWithURL:completionHandler:]` is called in Objective-C with explicitly passing - // `nil` as the `completionHandler` (it produces a warning, but compiles). - let task = previousImplementation(session, Self.selector, request, completionHandler) - interceptor.intercept(task: task, additionalFirstPartyHosts: delegate.firstPartyHosts) - return task - } - - var _task: URLSessionDataTask? - let request = interceptor.intercept(request: request, additionalFirstPartyHosts: delegate.firstPartyHosts) - let task = previousImplementation(session, Self.selector, request) { data, response, error in - completionHandler(data, response, error) - - if let task = _task { // sanity check, should always succeed - data.map { interceptor.task(task, didReceive: $0) } - interceptor.task(task, didCompleteWithError: error) - } - } - - _task = task - interceptor.intercept(task: task, additionalFirstPartyHosts: delegate.firstPartyHosts) - return task - } - } - } - } - - /// Swizzles the `URLSession.dataTask(with:completionHandler:)` for `URL`. - class DataTaskWithURLAndCompletion: MethodSwizzler< - @convention(c) (URLSession, Selector, URL, CompletionHandler?) -> URLSessionDataTask, - @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask - > { - private static let selector = #selector( - URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URL, @escaping CompletionHandler) -> URLSessionDataTask - ) - - private let method: FoundMethod - - static func build() throws -> DataTaskWithURLAndCompletion { - return try DataTaskWithURLAndCompletion( - selector: self.selector, - klass: URLSession.self - ) - } - - private init(selector: Selector, klass: AnyClass) throws { - self.method = try Self.findMethod(with: selector, in: klass) - super.init() - } - - func swizzle() { - typealias Signature = @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask - swizzle(method) { previousImplementation -> Signature in - return { session, url, completionHandler -> URLSessionDataTask in - guard - let delegate = (session.delegate as? __URLSessionDelegateProviding)?.ddURLSessionDelegate, - let interceptor = delegate.interceptor - else { - return previousImplementation(session, Self.selector, url, completionHandler) - } - - guard let completionHandler = completionHandler else { - let task = previousImplementation(session, Self.selector, url, completionHandler) - interceptor.intercept(task: task, additionalFirstPartyHosts: delegate.firstPartyHosts) - return task - } - - var _task: URLSessionDataTask? - let task = previousImplementation(session, Self.selector, url) { data, response, error in - completionHandler(data, response, error) - - if let task = _task { // sanity check, should always succeed - data.map { interceptor.task(task, didReceive: $0) } - interceptor.task(task, didCompleteWithError: error) - } - } - - _task = task - interceptor.intercept(task: task, additionalFirstPartyHosts: delegate.firstPartyHosts) - return task - } - } - } - } - - /// Swizzles the `URLSession.dataTask(with:)` for `URLRequest`. - class DataTaskWithURLRequest: MethodSwizzler< - @convention(c) (URLSession, Selector, URLRequest) -> URLSessionDataTask, - @convention(block) (URLSession, URLRequest) -> URLSessionDataTask - > { - private static let selector = #selector( - URLSession.dataTask(with:) as (URLSession) -> (URLRequest) -> URLSessionDataTask - ) - - private let method: FoundMethod - - static func build() throws -> DataTaskWithURLRequest { - return try DataTaskWithURLRequest( - selector: self.selector, - klass: URLSession.self - ) - } - - private init(selector: Selector, klass: AnyClass) throws { - self.method = try Self.findMethod(with: selector, in: klass) - super.init() - } - - func swizzle() { - typealias Signature = @convention(block) (URLSession, URLRequest) -> URLSessionDataTask - swizzle(method) { previousImplementation -> Signature in - return { session, request -> URLSessionDataTask in - guard let delegate = (session.delegate as? __URLSessionDelegateProviding)?.ddURLSessionDelegate else { - return previousImplementation(session, Self.selector, request) - } - - let request = delegate.interceptor?.intercept(request: request, additionalFirstPartyHosts: delegate.firstPartyHosts) ?? request - let task = previousImplementation(session, Self.selector, request) - if #available(iOS 13.0, *) { - // Prior to iOS 13.0, `dataTask(with:)` (for `URLRequest`) calls the - // the `dataTask(with:completionHandler:)` (for `URLRequest`) internally, - // so the task creation will be notified from `dataTaskWithURLRequestAndCompletion` swizzling. - delegate.interceptor?.intercept(task: task, additionalFirstPartyHosts: delegate.firstPartyHosts) - } - return task - } - } - } - } - - /// Swizzles the `URLSession.dataTask(with:)` for `URL`. - class DataTaskWithURL: MethodSwizzler< - @convention(c) (URLSession, Selector, URL) -> URLSessionDataTask, - @convention(block) (URLSession, URL) -> URLSessionDataTask - > { - private static let selector = #selector( - URLSession.dataTask(with:) as (URLSession) -> (URL) -> URLSessionDataTask - ) - - private let method: FoundMethod - - static func build() throws -> DataTaskWithURL { - return try DataTaskWithURL( - selector: self.selector, - klass: URLSession.self - ) - } - - private init(selector: Selector, klass: AnyClass) throws { - self.method = try Self.findMethod(with: selector, in: klass) - super.init() - } - - func swizzle() { - typealias Signature = @convention(block) (URLSession, URL) -> URLSessionDataTask - swizzle(method) { previousImplementation -> Signature in - return { session, url -> URLSessionDataTask in - let task = previousImplementation(session, Self.selector, url) - if let delegate = (session.delegate as? __URLSessionDelegateProviding)?.ddURLSessionDelegate { - delegate.interceptor?.intercept(task: task, additionalFirstPartyHosts: delegate.firstPartyHosts) - } + let interceptedRequest = interceptRequest(request) ?? request + let task = previousImplementation(session, Self.selector, interceptedRequest, completionHandler) + interceptTask(task) return task } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift new file mode 100644 index 0000000000..befdfd1673 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +private var sessionFirstPartyHostsKey: UInt8 = 31 +internal extension URLSessionTask { + /// Returns the first party hosts for this task. + var firstPartyHosts: FirstPartyHosts { + get { + return sessionFirstPartyHosts ?? .init() + } + } + + /// Extension property for storing first party hosts passed from `URLSession` to `URLSessionTask`. + /// This is used for `URLSessionTask` based APIs. + var sessionFirstPartyHosts: FirstPartyHosts? { + set { + objc_setAssociatedObject(self, &sessionFirstPartyHostsKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) + } + get { + return objc_getAssociatedObject(self, &sessionFirstPartyHostsKey) as? FirstPartyHosts + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegate+Tracking.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegate+Tracking.swift new file mode 100644 index 0000000000..714a8cec8e --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegate+Tracking.swift @@ -0,0 +1,27 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +private var coreKey: UInt8 = 43 + +public extension URLSessionDelegate { + /// Returns the `DatadogCore` for this delegate. + weak var core: DatadogCoreProtocol? { + set { + objc_setAssociatedObject(self, &coreKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN) + } + get { + return objc_getAssociatedObject(self, &coreKey) as? DatadogCoreProtocol + } + } + + /// Returns the `URLSessionInterceptor` for this delegate. + var interceptor: URLSessionInterceptor? { + let core = self.core ?? CoreRegistry.default + return URLSessionInterceptor.shared(in: core) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegateSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegateSwizzler.swift new file mode 100644 index 0000000000..b3c8df2deb --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegateSwizzler.swift @@ -0,0 +1,199 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// Swizzles `URLSessionTaskDelegate` callbacks. +internal class URLSessionTaskDelegateSwizzler { + private static var _didFinishCollectingMap: [String: DidFinishCollecting?] = [:] + static var didFinishCollectingMap: [String: DidFinishCollecting?] { + get { + lock.lock() + defer { lock.unlock() } + return _didFinishCollectingMap + } + set { + lock.lock() + defer { lock.unlock() } + _didFinishCollectingMap = newValue + } + } + private static var lock = NSRecursiveLock() + + private static var _didCompleteWithErrorMap: [String: DidCompleteWithError?] = [:] + static var didCompleteWithErrorMap: [String: DidCompleteWithError?] { + get { + lock.lock() + defer { lock.unlock() } + return _didCompleteWithErrorMap + } + set { + lock.lock() + defer { lock.unlock() } + _didCompleteWithErrorMap = newValue + } + } + + static var isBinded: Bool { + lock.lock() + defer { lock.unlock() } + return didFinishCollectingMap.isEmpty == false || didCompleteWithErrorMap.isEmpty == false + } + + static func bindIfNeeded( + delegateClass: AnyClass, + interceptDidFinishCollecting: @escaping (URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void, + interceptDidCompleteWithError: @escaping (URLSession, URLSessionTask, Error?) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + + guard isBinded == false else { + return + } + + try bind( + delegateClass: delegateClass, + interceptDidFinishCollecting: interceptDidFinishCollecting, + interceptDidCompleteWithError: interceptDidCompleteWithError + ) + } + + static func bind( + delegateClass: AnyClass, + interceptDidFinishCollecting: @escaping (URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void, + interceptDidCompleteWithError: @escaping (URLSession, URLSessionTask, Error?) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + + let didFinishCollecting = try DidFinishCollecting.build(klass: delegateClass) + let key = MetaTypeExtensions.key(from: delegateClass) + + didFinishCollecting.swizzle(intercept: interceptDidFinishCollecting) + didFinishCollectingMap[key] = didFinishCollecting + + let didCompleteWithError = try DidCompleteWithError.build(klass: delegateClass) + didCompleteWithError.swizzle(intercept: interceptDidCompleteWithError) + didCompleteWithErrorMap[key] = didCompleteWithError + } + + static func unbind(delegateClass: AnyClass) { + lock.lock() + defer { lock.unlock() } + + let key = MetaTypeExtensions.key(from: delegateClass) + didFinishCollectingMap[key]??.unswizzle() + didFinishCollectingMap[key] = nil + + didCompleteWithErrorMap[key]??.unswizzle() + didCompleteWithErrorMap[key] = nil + } + + static func unbindAll() { + lock.lock() + defer { lock.unlock() } + + didFinishCollectingMap.forEach { _, didFinishCollecting in + didFinishCollecting?.unswizzle() + } + didFinishCollectingMap.removeAll() + + didCompleteWithErrorMap.forEach { _, didCompleteWithError in + didCompleteWithError?.unswizzle() + } + didCompleteWithErrorMap.removeAll() + } + + /// Swizzles `URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)` method. + class DidFinishCollecting: MethodSwizzler<@convention(c) (URLSessionTaskDelegate, Selector, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void, @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void> { + private static let selector = #selector(URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)) + + private let method: FoundMethod + + static func build(klass: AnyClass) throws -> DidFinishCollecting { + return try DidFinishCollecting(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + do { + method = try Self.findMethod(with: selector, in: klass) + } catch { + // URLSessionTaskDelegate doesn't implement the selector, so we inject it and swizzle it + let block: @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void = { delegate, session, task, metrics in + } + let imp = imp_implementationWithBlock(block) + /* + v@:@@@ means: + v - return type is void + @ - self + : - selector + @ - first argument is an object + @ - second argument is an object + @ - third argument is an object + */ + class_addMethod(klass, selector, imp, "v@:@@@") + method = try Self.findMethod(with: selector, in: klass) + } + + super.init() + } + + func swizzle(intercept: @escaping (URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void) { + typealias Signature = @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void + swizzle(method) { previousImplementation -> Signature in + return { delegate, session, task, metrics in + intercept(session, task, metrics) + return previousImplementation(delegate, Self.selector, session, task, metrics) + } + } + } + } + + class DidCompleteWithError: MethodSwizzler<@convention(c) (URLSessionTaskDelegate, Selector, URLSession, URLSessionTask, Error?) -> Void, @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, Error?) -> Void> { + private static let selector = #selector(URLSessionTaskDelegate.urlSession(_:task:didCompleteWithError:)) + + private let method: FoundMethod + + static func build(klass: AnyClass) throws -> DidCompleteWithError { + return try DidCompleteWithError(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + do { + method = try Self.findMethod(with: selector, in: klass) + } catch { + // URLSessionTaskDelegate doesn't implement the selector, so we inject it and swizzle it + let block: @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, Error?) -> Void = { delegate, session, task, error in + } + let imp = imp_implementationWithBlock(block) + /* + v@:@@@ means: + v - return type is void + @ - self + : - selector + @ - first argument is an object + @ - second argument is an object + @ - third argument is an object + */ + class_addMethod(klass, selector, imp, "v@:@@@") + method = try Self.findMethod(with: selector, in: klass) + } + + super.init() + } + + func swizzle(intercept: @escaping (URLSession, URLSessionTask, Error?) -> Void) { + typealias Signature = @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, Error?) -> Void + swizzle(method) { previousImplementation -> Signature in + return { delegate, session, task, error in + intercept(session, task, error) + return previousImplementation(delegate, Self.selector, session, task, error) + } + } + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift index 9a6922c94f..375da553d5 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift @@ -11,7 +11,7 @@ public class URLSessionTaskInterception { public let identifier: UUID /// The initial request send during this interception. It is, the request send from `URLSession`, not the one /// given by the user (as the request could have been modified in `URLSessionSwizzler`). - public let request: URLRequest + public private(set) var request: URLRequest /// Tells if the `request` is send to a 1st party host. public let isFirstPartyRequest: Bool /// Task metrics collected during this interception. @@ -50,6 +50,10 @@ public class URLSessionTaskInterception { } } + func register(request: URLRequest) { + self.request = request + } + func register(response: URLResponse?, error: Error?) { self.completion = ResourceCompletion( response: response as? HTTPURLResponse, diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift new file mode 100644 index 0000000000..90b50f1841 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift @@ -0,0 +1,85 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// Swizzles `URLSessionTask` methods. +internal class URLSessionTaskSwizzler { + private static var _resume: Resume? + static var resume: Resume? { + get { + lock.lock() + defer { lock.unlock() } + return _resume + } + set { + lock.lock() + defer { lock.unlock() } + _resume = newValue + } + } + + private static var lock = NSRecursiveLock() + + static var isBinded: Bool { + lock.lock() + defer { lock.unlock() } + return resume != nil + } + + static func bindIfNeeded(interceptResume: @escaping (URLSessionTask) -> Void) throws { + lock.lock() + defer { lock.unlock() } + + guard resume == nil else { + return + } + + try bind(interceptResume: interceptResume) + } + + static func bind(interceptResume: @escaping (URLSessionTask) -> Void) throws { + lock.lock() + defer { lock.unlock() } + + self.resume = try Resume.build() + + resume?.swizzle(intercept: interceptResume) + } + + static func unbind() { + lock.lock() + defer { lock.unlock() } + resume?.unswizzle() + resume = nil + } + + /// Swizzles `URLSessionTask.resume()` method. + class Resume: MethodSwizzler<@convention(c) (URLSessionTask, Selector) -> Void, @convention(block) (URLSessionTask) -> Void> { + private static let selector = #selector(URLSessionTask.resume) + + private let method: FoundMethod + + static func build() throws -> Resume { + return try Resume(selector: self.selector, klass: URLSessionTask.self) + } + + private init(selector: Selector, klass: AnyClass) throws { + self.method = try Self.findMethod(with: selector, in: klass) + super.init() + } + + func swizzle(intercept: @escaping (URLSessionTask) -> Void) { + typealias Signature = @convention(block) (URLSessionTask) -> Void + swizzle(method) { previousImplementation -> Signature in + return { task in + intercept(task) + previousImplementation(task, Self.selector) + } + } + } + } +} diff --git a/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift b/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift index 57f4f0c83b..028987633f 100644 --- a/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift +++ b/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift @@ -6,6 +6,12 @@ import Foundation +public enum Swizzling { + /// The list of active swizzlings to ensure integrity in unit tests. + @ReadWriteLock + public static var activeSwizzlingNames: [String] = [] +} + open class MethodSwizzler { public struct FoundMethod: Hashable { let method: Method @@ -70,7 +76,7 @@ open class MethodSwizzler { set(newIMP: newImp, for: foundMethod) #if DD_SDK_COMPILED_FOR_TESTING - activeSwizzlingNames.append(foundMethod.swizzlingName) + Swizzling.activeSwizzlingNames.append(foundMethod.swizzlingName) #endif } } @@ -82,7 +88,7 @@ open class MethodSwizzler { let originalIMP: IMP = unsafeBitCast(originalTypedIMP, to: IMP.self) method_setImplementation(foundMethod.method, originalIMP) - activeSwizzlingNames.removeAll { $0 == foundMethod.swizzlingName } + Swizzling.activeSwizzlingNames.removeAll { $0 == foundMethod.swizzlingName } } } @@ -124,6 +130,3 @@ open class MethodSwizzler { internal extension MethodSwizzler.FoundMethod { var swizzlingName: String { "\(klass).\(method_getName(method))" } } - -/// The list of active swizzlings to ensure integrity in unit tests. -internal var activeSwizzlingNames: [String] = [] diff --git a/DatadogInternal/Sources/Utils/MetaTypeExtensions.swift b/DatadogInternal/Sources/Utils/MetaTypeExtensions.swift new file mode 100644 index 0000000000..11079814be --- /dev/null +++ b/DatadogInternal/Sources/Utils/MetaTypeExtensions.swift @@ -0,0 +1,38 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal enum MetaTypeExtensions { + static func key(from anyClass: Any) -> String { + let fullName = String(reflecting: anyClass) + + // fullName may have infix like "(unknown context at $1130f7094)", remove everything inside parenthesis including parenthesis + // Read more: https://github.com/apple/swift/issues/49336 + var key = "" + var parenthesisCount = 0 + for char in fullName { + if char == "(" { + parenthesisCount += 1 + } else if char == ")" { + parenthesisCount -= 1 + } else if parenthesisCount == 0 { + key.append(char) + } + } + + // replace multiple consecutive . with single . + var sanitizedKey = "" + for char in key { + if sanitizedKey.last == "." && char == "." { + continue + } + sanitizedKey.append(char) + } + + return sanitizedKey + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift index 24684b92d0..f14488a1d2 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift @@ -24,22 +24,28 @@ class NetworkInstrumentationFeatureTests: XCTestCase { } override func tearDown() { + core?.get(feature: NetworkInstrumentationFeature.self)?.unbindAll() core = nil super.tearDown() } // MARK: - Interception Flow - func testGivenURLSessionWithDatadogDelegate_whenUsingTaskWithURL_itNotifiesInterceptor() { - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") + func testGivenURLSessionWithDatadogDelegate_whenUsingTaskWithURL_itNotifiesInterceptor() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } + handler.onInterceptionDidStart = { _ in + notifyInterceptionDidStart.fulfill() + } + handler.onInterceptionDidComplete = { _ in + notifyInterceptionDidComplete.fulfill() + } // Given - let delegate = DatadogURLSessionDelegate(in: core) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) let session = server.getInterceptedURLSession(delegate: delegate) // When @@ -49,31 +55,32 @@ class NetworkInstrumentationFeatureTests: XCTestCase { // Then wait( for: [ - notifyInterceptionStart, - notifyInterceptionComplete + notifyInterceptionDidStart, + notifyInterceptionDidComplete ], - timeout: 0.5, + timeout: 5, enforceOrder: true ) _ = server.waitAndReturnRequests(count: 1) } - func testGivenURLSessionWithDatadogDelegate_whenUsingTaskWithURLRequest_itNotifiesInterceptor() { + func testGivenURLSessionWithDatadogDelegate_whenUsingTaskWithURLRequest_itNotifiesInterceptor() throws { let notifyRequestMutation = expectation(description: "Notify request mutation") - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) handler.onRequestMutation = { _, _ in notifyRequestMutation.fulfill() } - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } // Given let url: URL = .mockAny() handler.firstPartyHosts = .init( hostsWithTracingHeaderTypes: [url.host!: [.datadog]] ) - let delegate = DatadogURLSessionDelegate(in: core) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) let session = server.getInterceptedURLSession(delegate: delegate) // When @@ -85,33 +92,116 @@ class NetworkInstrumentationFeatureTests: XCTestCase { wait( for: [ notifyRequestMutation, - notifyInterceptionStart, - notifyInterceptionComplete + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + _ = server.waitAndReturnRequests(count: 1) + } + + @available(iOS 13.0, tvOS 13.0, *) + func testGivenURLSessionWithCustomDelegate_whenUsingAsyncDataFromURL_itNotifiesInterceptor() async throws { + /// Testing only 16.0 or above because 15.0 has ThreadSanitizer issues with async APIs + guard #available(iOS 16, tvOS 16, *) else { + return + } + + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + let server = ServerMock( + delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10)), + skipIsMainThreadCheck: true + ) + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + _ = try await session.data(from: URL.mockAny(), delegate: delegate) + + // Then + await dd_fulfillment( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + + _ = server.waitAndReturnRequests(count: 1) + } + + @available(iOS 13.0, tvOS 13.0, *) + func testGivenURLSessionWithCustomDelegate_whenUsingAsyncDataForURLRequest_itNotifiesInterceptor() async throws { + /// Testing only 16.0 or above because 15.0 has ThreadSanitizer issues with async APIs + guard #available(iOS 16, tvOS 16, *) else { + return + } + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + let server = ServerMock( + delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10)), + skipIsMainThreadCheck: true + ) + + handler.onInterceptionDidStart = { interception in + XCTAssertTrue(interception.isFirstPartyRequest) + notifyInterceptionDidStart.fulfill() + } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + // Given + let url: URL = .mockAny() + handler.firstPartyHosts = .init( + hostsWithTracingHeaderTypes: [url.host!: [.datadog]] + ) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + _ = try await session.data(for: URLRequest(url: url), delegate: delegate) + + // Then + await dd_fulfillment( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete ], - timeout: 0.5, + timeout: 5, enforceOrder: true ) + _ = server.waitAndReturnRequests(count: 1) } // MARK: - Interception Values func testGivenURLSessionWithDatadogDelegate_whenTaskCompletesWithFailure_itPassesAllValuesToTheInterceptor() throws { - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") - notifyInterceptionStart.expectedFulfillmentCount = 2 - notifyInterceptionComplete.expectedFulfillmentCount = 2 + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 let expectedError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "some error"]) let server = ServerMock(delivery: .failure(error: expectedError)) - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } let dateBeforeAnyRequests = Date() // Given - let delegate = DatadogURLSessionDelegate(in: core) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) let session = server.getInterceptedURLSession(delegate: delegate) // When @@ -126,7 +216,7 @@ class NetworkInstrumentationFeatureTests: XCTestCase { .resume() // Then - waitForExpectations(timeout: 1, handler: nil) + waitForExpectations(timeout: 5, handler: nil) _ = server.waitAndReturnRequests(count: 1) let dateAfterAllRequests = Date() @@ -144,21 +234,22 @@ class NetworkInstrumentationFeatureTests: XCTestCase { } func testGivenURLSessionWithDatadogDelegate_whenTaskCompletesWithSuccess_itPassesAllValuesToTheInterceptor() throws { - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") - notifyInterceptionStart.expectedFulfillmentCount = 2 - notifyInterceptionComplete.expectedFulfillmentCount = 2 + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 let randomData: Data = .mockRandom() let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: randomData)) - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } let dateBeforeAnyRequests = Date() // Given - let delegate = DatadogURLSessionDelegate(in: core) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) let session = server.getInterceptedURLSession(delegate: delegate) // When @@ -173,7 +264,7 @@ class NetworkInstrumentationFeatureTests: XCTestCase { .resume() // Then - waitForExpectations(timeout: 0.5, handler: nil) + waitForExpectations(timeout: 5, handler: nil) _ = server.waitAndReturnRequests(count: 1) let dateAfterAllRequests = Date() @@ -191,6 +282,64 @@ class NetworkInstrumentationFeatureTests: XCTestCase { } } + @available(iOS 13.0, tvOS 13.0, *) + func testGivenURLSessionWithCustomDelegate_whenUsingAsyncData_itPassesAllValuesToTheInterceptor() async throws { + /// Testing only 16.0 or above because 15.0 has ThreadSanitizer issues with async APIs + guard #available(iOS 16, tvOS 16, *) else { + return + } + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 + + let expectedError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "some error"]) + let server = ServerMock( + delivery: .failure(error: expectedError), + skipIsMainThreadCheck: true + ) + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + let dateBeforeAnyRequests = Date() + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + let url1: URL = .mockRandom() + _ = try? await session.data(from: url1, delegate: delegate) + + let url2: URL = .mockRandom() + _ = try? await session.data(for: URLRequest(url: url2), delegate: delegate) + + // Then + await dd_fulfillment( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + + _ = server.waitAndReturnRequests(count: 2) + + let dateAfterAllRequests = Date() + + XCTAssertEqual(handler.interceptions.count, 2, "Interceptor should record metrics for 2 tasks") + + handler.interceptions.forEach { id, interception in + XCTAssertGreaterThan(interception.metrics?.fetch.start ?? .distantPast, dateBeforeAnyRequests) + XCTAssertLessThan(interception.metrics?.fetch.end ?? .distantFuture, dateAfterAllRequests) + XCTAssertNil(interception.data, "Data should not be recorded for \(id)") + XCTAssertEqual((interception.completion?.error as? NSError)?.localizedDescription, "some error") + } + } + // MARK: - Usage func testItCanBeInitializedBeforeInitializingDefaultSDKCore() throws { @@ -328,21 +477,21 @@ class NetworkInstrumentationFeatureTests: XCTestCase { // MARK: - First Party Hosts func testGivenHandler_whenInterceptingRequests_itDetectFirstPartyHost() throws { - let notifyInterceptionStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) // Given - let delegate = DatadogURLSessionDelegate( - in: core, - additionalFirstPartyHostsWithHeaderTypes: ["test.com": [.datadog]] - ) + let delegate = MockDelegate() + let firstPartyHosts: URLSessionInstrumentation.FirstPartyHostsTracing = .traceWithHeaders(hostsWithHeaders: ["test.com": [.datadog]]) + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self, firstPartyHostsTracing: firstPartyHosts), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) let request: URLRequest = .mockWith(url: "https://test.com") - handler.onInterceptionStart = { + handler.onInterceptionDidStart = { // Then XCTAssertTrue($0.isFirstPartyRequest) - notifyInterceptionStart.fulfill() + notifyInterceptionDidStart.fulfill() } // When @@ -351,29 +500,29 @@ class NetworkInstrumentationFeatureTests: XCTestCase { .resume() // Then - waitForExpectations(timeout: 1, handler: nil) + waitForExpectations(timeout: 5, handler: nil) _ = server.waitAndReturnRequests(count: 1) } func testGivenDelegateSubclass_whenInterceptingRequests_itDetectFirstPartyHost() throws { - let notifyInterceptionStart = expectation(description: "Notify interception did start") - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) // Given - class SubclassDelegate: DatadogURLSessionDelegate {} - let delegate = SubclassDelegate( + let delegate = DatadogURLSessionDelegate( in: core, additionalFirstPartyHostsWithHeaderTypes: ["test.com": [.datadog]] ) + let session = server.getInterceptedURLSession(delegate: delegate) let request: URLRequest = .mockWith(url: "https://test.com") - handler.onInterceptionStart = { + handler.onInterceptionDidStart = { // Then XCTAssertTrue($0.isFirstPartyRequest) - notifyInterceptionStart.fulfill() + notifyInterceptionDidStart.fulfill() } // When @@ -382,13 +531,13 @@ class NetworkInstrumentationFeatureTests: XCTestCase { .resume() // Then - waitForExpectations(timeout: 1, handler: nil) + waitForExpectations(timeout: 5, handler: nil) _ = server.waitAndReturnRequests(count: 1) } func testGivenCompositeDelegate_whenInterceptingRequests_itDetectFirstPartyHost() throws { - let notifyInterceptionStart = expectation(description: "Notify interception did start") - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) // Given @@ -409,10 +558,10 @@ class NetworkInstrumentationFeatureTests: XCTestCase { let session = server.getInterceptedURLSession(delegate: delegate) let request: URLRequest = .mockWith(url: "https://test.com") - handler.onInterceptionStart = { + handler.onInterceptionDidStart = { // Then XCTAssertTrue($0.isFirstPartyRequest) - notifyInterceptionStart.fulfill() + notifyInterceptionDidStart.fulfill() } // When @@ -421,7 +570,7 @@ class NetworkInstrumentationFeatureTests: XCTestCase { .resume() // Then - waitForExpectations(timeout: 0.5, handler: nil) + waitForExpectations(timeout: 5, handler: nil) _ = server.waitAndReturnRequests(count: 1) } @@ -451,4 +600,7 @@ class NetworkInstrumentationFeatureTests: XCTestCase { ) // swiftlint:enable opening_brace trailing_closure } + + class MockDelegate: NSObject, URLSessionDataDelegate { + } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDataDelegateSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDataDelegateSwizzlerTests.swift new file mode 100644 index 0000000000..a1a7d9b5ce --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDataDelegateSwizzlerTests.swift @@ -0,0 +1,88 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogInternal + +final class URLSessionDataDelegateSwizzlerTests: XCTestCase { + override func tearDown() { + URLSessionDataDelegateSwizzler.unbind(delegateClass: MockDelegate.self) + XCTAssertEqual(URLSessionDataDelegateSwizzler.didReceiveMap.count, 0) + + super.tearDown() + } + + func testSwizzling_whenDidReceiveDataIsImplemented() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + } + } + + let delegate = MockDelegate() + let expectation = XCTestExpectation(description: "didReceiveData") + + try URLSessionDataDelegateSwizzler.bind(delegateClass: MockDelegate.self) { _, _, _ in + expectation.fulfill() + } + + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let task = session.dataTask(with: URL(string: "https://www.datadoghq.com/")!) + task.resume() + + wait(for: [expectation], timeout: 5) + } + + func testSwizzling_whenDidReceiveDataNotImplemented() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + } + + let delegate = MockDelegate() + let expectation = XCTestExpectation(description: "didReceiveData") + + try URLSessionDataDelegateSwizzler.bind(delegateClass: MockDelegate.self) { _, _, _ in + expectation.fulfill() + } + + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let task = session.dataTask(with: URL(string: "https://www.datadoghq.com/")!) + task.resume() + + wait(for: [expectation], timeout: 5) + } + + func testBindings() throws { + XCTAssertNil(URLSessionDataDelegateSwizzler.didReceiveMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + + try URLSessionDataDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidReceive: { _, _, _ in }) + XCTAssertNotNil(URLSessionDataDelegateSwizzler.didReceiveMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + + try URLSessionDataDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidReceive: { _, _, _ in }) + XCTAssertNotNil(URLSessionDataDelegateSwizzler.didReceiveMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + + URLSessionDataDelegateSwizzler.unbind(delegateClass: MockDelegate.self) + XCTAssertNil(URLSessionDataDelegateSwizzler.didReceiveMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + } + + func testConcurrentBinding() throws { + // swiftlint:disable opening_brace trailing_closure + callConcurrently( + closures: [ + { try? URLSessionDataDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidReceive: self.intercept(_:dataTask:didReceive:)) }, + { URLSessionDataDelegateSwizzler.unbind(delegateClass: MockDelegate.self) }, + { try? URLSessionDataDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidReceive: self.intercept(_:dataTask:didReceive:)) }, + { URLSessionDataDelegateSwizzler.unbind(delegateClass: MockDelegate.self) }, + ], + iterations: 50 + ) + // swiftlint:enable opening_brace trailing_closure + } + + func intercept(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + } + + class MockDelegate: NSObject, URLSessionDataDelegate { + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift index e1524d72fd..ae956a39ef 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift @@ -5,389 +5,103 @@ */ import XCTest -import TestUtilities @testable import DatadogInternal -class URLSessionSwizzlerTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional - private var core: SingleFeatureCoreMock! - private var handler: URLSessionHandlerMock! - // swiftlint:enable implicitly_unwrapped_optional - - override func setUpWithError() throws { - try super.setUpWithError() - - core = SingleFeatureCoreMock() - handler = URLSessionHandlerMock() - - try core.register(urlSessionHandler: handler) - } - +final class URLSessionSwizzlerTests: XCTestCase { override func tearDown() { - core = nil - handler = nil - super.tearDown() - } - - // MARK: - Binding - - func testBindings() throws { - func AssertSwizzlingEnable() { - XCTAssertNotNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion) - XCTAssertNotNil(URLSessionSwizzler.dataTaskWithURLRequest) - if #available(iOS 13.0, *) { - XCTAssertNotNil(URLSessionSwizzler.dataTaskWithURLAndCompletion) - XCTAssertNotNil(URLSessionSwizzler.dataTaskWithURL) - } - } - - func AssertSwizzlingDisable() { - XCTAssertEqual(URLSessionSwizzler.bindingsCount, 0) - XCTAssertNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion) - XCTAssertNil(URLSessionSwizzler.dataTaskWithURLRequest) - XCTAssertNil(URLSessionSwizzler.dataTaskWithURLAndCompletion) - XCTAssertNil(URLSessionSwizzler.dataTaskWithURL) - } - - // binding from `core` - XCTAssertEqual(URLSessionSwizzler.bindingsCount, 1) - AssertSwizzlingEnable() - - try URLSessionSwizzler.bind() - XCTAssertEqual(URLSessionSwizzler.bindingsCount, 2) - AssertSwizzlingEnable() - URLSessionSwizzler.unbind() - XCTAssertEqual(URLSessionSwizzler.bindingsCount, 1) - AssertSwizzlingEnable() + XCTAssertNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion as Any?) - URLSessionSwizzler.unbind() - AssertSwizzlingDisable() - - URLSessionSwizzler.unbind() - XCTAssertEqual(URLSessionSwizzler.bindingsCount, 0) - AssertSwizzlingDisable() + super.tearDown() } - // MARK: - Interception Flow - - func testGivenURLSessionWithDDURLSessionDelegate_whenUsingTaskWithURLRequestAndCompletion_itNotifiesCreationAndCompletionAndModifiesTheRequest() throws { - let notifyRequestMutation = expectation(description: "Notify request mutation") - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") - let completionHandlerCalled = expectation(description: "Call completion handler") - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) - - handler.modifiedRequest = URLRequest(url: .mockRandom()) - handler.onRequestMutation = { _, _ in notifyRequestMutation.fulfill() } - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } + func testSwizzling_dataTaskWithURLRequestAndCompletion() throws { + let didInterceptRequest = XCTestExpectation(description: "interceptURLRequest") + let didInterceptTask = XCTestExpectation(description: "interceptTask") + try URLSessionSwizzler.bind(interceptURLRequest: { request in + didInterceptRequest.fulfill() + return self.interceptRequest(request: request) + }, interceptTask: { _ in + didInterceptTask.fulfill() + }) - // Given - let url: URL = .mockRandom() - handler.firstPartyHosts = .init( - hostsWithTracingHeaderTypes: [url.host!: [.datadog]] - ) - let delegate = DatadogURLSessionDelegate(in: core) - let session = server.getInterceptedURLSession(delegate: delegate) - - // When - session - .dataTask(with: URLRequest(url: url)) { _, _, _ in completionHandlerCalled.fulfill() } - .resume() + let session = URLSession(configuration: .default) + let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!) + let task = session.dataTask(with: request) { _, _, _ in } + task.resume() - // Then - wait(for: [completionHandlerCalled], timeout: 1) wait( for: [ - notifyRequestMutation, - notifyInterceptionStart, - notifyInterceptionComplete + didInterceptRequest, + didInterceptTask ], - timeout: 1, + timeout: 5, enforceOrder: true ) - - let requestSent = try XCTUnwrap(server.waitAndReturnRequests(count: 1).first) - XCTAssertEqual(requestSent, handler.modifiedRequest, "The request should be modified") } - func testGivenURLSessionWithDDURLSessionDelegate_whenUsingTaskWithURLAndCompletion_itNotifiesTaskCreationAndCompletionAndModifiesTheRequestOnlyPriorToIOS13() throws { - let notifyRequestMutation = expectation(description: "Notify request mutation") - if #available(iOS 13.0, *) { - notifyRequestMutation.isInverted = true - } - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") - let completionHandlerCalled = expectation(description: "Call completion handler") - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) - - handler.modifiedRequest = URLRequest(url: .mockRandom()) - handler.onRequestMutation = { _, _ in notifyRequestMutation.fulfill() } - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } - - // Given - let url: URL = .mockRandom() - handler.firstPartyHosts = .init( - hostsWithTracingHeaderTypes: [url.host!: [.datadog]] - ) - let delegate = DatadogURLSessionDelegate(in: core) - let session = server.getInterceptedURLSession(delegate: delegate) - - // When - session - .dataTask(with: url) { _, _, _ in completionHandlerCalled.fulfill() } - .resume() - - // Then - wait(for: [completionHandlerCalled], timeout: 1) - wait( - for: [ - notifyRequestMutation, - notifyInterceptionStart, - notifyInterceptionComplete - ], - timeout: 2, - enforceOrder: true - ) - - let requestSent = try XCTUnwrap(server.waitAndReturnRequests(count: 1).first) + func testSwizzling_testSwizzling_dataTaskWithURLRequest() throws { + // runs only on iOS 12 or below + // because on iOS 12 and below `URLSession.dataTask(with:)` is implemented using `URLSession.dataTask(with:completionHandler:)` if #available(iOS 13.0, *) { - XCTAssertNotEqual(requestSent, handler.modifiedRequest, "The request should not be modified on iOS 13.0 and above.") - } else { - XCTAssertEqual(requestSent, handler.modifiedRequest, "The request should be modified prior to iOS 13.0.") + return } - } - - func testGivenURLSessionWithDDURLSessionDelegate_whenUsingTaskWithURLRequest_itNotifiesCreationAndCompletionAndModifiesTheRequest() throws { - let notifyRequestMutation = expectation(description: "Notify request mutation") - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) - - handler.modifiedRequest = URLRequest(url: .mockRandom()) - handler.onRequestMutation = { _, _ in notifyRequestMutation.fulfill() } - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } - - // Given - let url: URL = .mockAny() - handler.firstPartyHosts = .init( - hostsWithTracingHeaderTypes: [url.host!: [.datadog]] - ) - let delegate = DatadogURLSessionDelegate(in: core) - let session = server.getInterceptedURLSession(delegate: delegate) - - // When - let task = session.dataTask(with: URLRequest(url: url)) - task.resume() - - // Then - wait( - for: [ - notifyRequestMutation, - notifyInterceptionStart, - notifyInterceptionComplete - ], - timeout: 2, - enforceOrder: true - ) - - let requestSent = try XCTUnwrap(server.waitAndReturnRequests(count: 1).first) - XCTAssertEqual(requestSent, handler.modifiedRequest, "The request should be modified.") - } - func testGivenURLSessionWithDDURLSessionDelegate_whenUsingTaskWithURL_itNotifiesCreationAndCompletionAndDoesNotModifyTheRequest() throws { - let notifyRequestMutation = expectation(description: "Notify request mutation") - notifyRequestMutation.isInverted = true - let notifyInterceptionStart = expectation(description: "Notify interception did start") - let notifyInterceptionComplete = expectation(description: "Notify intercepion did complete") - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) + let didInterceptRequest = XCTestExpectation(description: "interceptURLRequest") + let didInterceptTask = XCTestExpectation(description: "interceptTask") + try URLSessionSwizzler.bind(interceptURLRequest: { request in + didInterceptRequest.fulfill() + return self.interceptRequest(request: request) + }, interceptTask: { _ in + didInterceptTask.fulfill() + }) - handler.modifiedRequest = URLRequest(url: .mockRandom()) - handler.onRequestMutation = { _, _ in notifyRequestMutation.fulfill() } - handler.onInterceptionStart = { _ in notifyInterceptionStart.fulfill() } - handler.onInterceptionComplete = { _ in notifyInterceptionComplete.fulfill() } - - // Given - let delegate = DatadogURLSessionDelegate(in: core) - let session = server.getInterceptedURLSession(delegate: delegate) - - // When - let task = session.dataTask(with: URL.mockRandom()) + let session = URLSession(configuration: .default) + let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!) + let task = session.dataTask(with: request) task.resume() - // Then wait( for: [ - notifyRequestMutation, - notifyInterceptionStart, - notifyInterceptionComplete + didInterceptRequest, + didInterceptTask ], - timeout: 2, + timeout: 5, enforceOrder: true ) - - let requestSent = try XCTUnwrap(server.waitAndReturnRequests(count: 1).first) - XCTAssertNotEqual(requestSent, handler.modifiedRequest, "The request should not be modified.") - } - - func testGivenNonInterceptedSession_itDoesntCallInterceptor() throws { - let doNotModifyRequest = expectation(description: "Do not notify request modification") - doNotModifyRequest.isInverted = true - let doNotNotifyStart = expectation(description: "Do not notify task creation") - doNotNotifyStart.isInverted = true - - handler.onRequestMutation = { _, _ in doNotModifyRequest.fulfill() } - handler.onInterceptionStart = { _ in doNotNotifyStart.fulfill() } - - // Given - let session = URLSession(configuration: .default) - - // When - let taskWithURL = session.dataTask(with: URL.mockAny()) - let taskWithURLRequest = session.dataTask(with: URLRequest.mockAny()) - let taskWithURLWithCompletion = session.dataTask(with: URL.mockAny()) { _, _, _ in } - let taskWithURLRequestWithCompletion = session.dataTask(with: URLRequest.mockAny()) { _, _, _ in } - - // Then - waitForExpectations(timeout: 2, handler: nil) - XCTAssertNotNil(taskWithURL) - XCTAssertNotNil(taskWithURLRequest) - XCTAssertNotNil(taskWithURLWithCompletion) - XCTAssertNotNil(taskWithURLRequestWithCompletion) - } - - // MARK: - Interception Values - - func testGivenSuccessfulTask_whenUsingSwizzledAPIs_itPassesAllValuesToTheInterceptor() throws { - let completionHandlersCalled = expectation(description: "Call 2 completion handlers") - completionHandlersCalled.expectedFulfillmentCount = 2 - let notifyTaskCompleted = expectation(description: "Notify 4 tasks completion") - notifyTaskCompleted.expectedFulfillmentCount = 4 - - handler.onInterceptionComplete = { _ in notifyTaskCompleted.fulfill() } - - // Given - let expectedResponse: HTTPURLResponse = .mockResponseWith(statusCode: 200) - let expectedData: Data = .mockRandom() - let server = ServerMock(delivery: .success(response: expectedResponse, data: expectedData)) - let delegate = DatadogURLSessionDelegate(in: core) - let session = server.getInterceptedURLSession(delegate: delegate) - - // When - let url1: URL = .mockRandom() - session.dataTask(with: URLRequest(url: url1)) { data, response, error in - XCTAssertEqual(data, expectedData) - XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, expectedResponse.statusCode) - XCTAssertNil(error) - completionHandlersCalled.fulfill() - } - .resume() - - let url2: URL = .mockRandom() - session.dataTask(with: url2) { data, response, error in - XCTAssertEqual(data, expectedData) - XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, expectedResponse.statusCode) - XCTAssertNil(error) - completionHandlersCalled.fulfill() - } - .resume() - - let url3: URL = .mockRandom() - session - .dataTask(with: URLRequest(url: url3)) - .resume() - - let url4: URL = .mockRandom() - session - .dataTask(with: url4) - .resume() - - // Then - waitForExpectations(timeout: 2, handler: nil) - - _ = server.waitAndReturnRequests(count: 4) - XCTAssertEqual(handler.interceptions.count, 4, "Interceptor should record 4 tasks") - - try [url1, url2, url3, url4].forEach { url in - let interception = try handler.interception(for: url).unwrapOrThrow() - XCTAssertEqual(interception.data, expectedData) - XCTAssertNotNil(interception.completion) - XCTAssertNil(interception.completion?.error) - } } - func testGivenFailedTask_whenUsingSwizzledAPIs_itPassesAllValuesToTheInterceptor() throws { - let completionHandlersCalled = expectation(description: "Call 2 completion handlers") - completionHandlersCalled.expectedFulfillmentCount = 2 - let notifyTaskCompleted = expectation(description: "Notify 4 tasks completion") - notifyTaskCompleted.expectedFulfillmentCount = 4 - - handler.onInterceptionComplete = { _ in notifyTaskCompleted.fulfill() } - - // Given - let expectedError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "some error"]) - let server = ServerMock(delivery: .failure(error: expectedError)) - let delegate = DatadogURLSessionDelegate(in: core) - let session = server.getInterceptedURLSession(delegate: delegate) - - // When - let url1: URL = .mockRandom() - session.dataTask(with: URLRequest(url: url1)) { data, response, error in - XCTAssertNil(data) - XCTAssertNil(response) - XCTAssertEqual((error! as NSError).localizedDescription, "some error") - completionHandlersCalled.fulfill() - } - .resume() - - let url2: URL = .mockRandom() - session.dataTask(with: url2) { data, response, error in - XCTAssertNil(data) - XCTAssertNil(response) - XCTAssertEqual((error! as NSError).localizedDescription, "some error") - completionHandlersCalled.fulfill() - } - .resume() - - let url3: URL = .mockRandom() - session - .dataTask(with: URLRequest(url: url3)) - .resume() + func testBindings() { + XCTAssertNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion as Any?) - let url4: URL = .mockRandom() - session - .dataTask(with: url4) - .resume() + try? URLSessionSwizzler.bind(interceptURLRequest: self.interceptRequest(request:), interceptTask: self.interceptTask(task:)) + XCTAssertNotNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion as Any?) - // Then - waitForExpectations(timeout: 2, handler: nil) + try? URLSessionSwizzler.bind(interceptURLRequest: self.interceptRequest(request:), interceptTask: self.interceptTask(task:)) + XCTAssertNotNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion as Any?) - _ = server.waitAndReturnRequests(count: 4) - XCTAssertEqual(handler.interceptions.count, 4, "Interceptor should record completion of 4 tasks") - - try [url1, url2, url3, url4].forEach { url in - let interception = try handler.interception(for: url).unwrapOrThrow() - XCTAssertNil(interception.data, "Data should not be recorded for \(url)") - XCTAssertEqual((interception.completion?.error as? NSError)?.localizedDescription, "some error") - } + URLSessionSwizzler.unbind() + XCTAssertNil(URLSessionSwizzler.dataTaskWithURLRequestAndCompletion as Any?) } - // MARK: - Thread Safety - func testConcurrentBinding() throws { // swiftlint:disable opening_brace trailing_closure - callConcurrently( + callConcurrently( closures: [ - { try? URLSessionSwizzler.bind() }, + { try? URLSessionSwizzler.bind(interceptURLRequest: self.interceptRequest(request:), interceptTask: self.interceptTask(task:)) }, + { URLSessionSwizzler.unbind() }, + { try? URLSessionSwizzler.bind(interceptURLRequest: self.interceptRequest(request:), interceptTask: self.interceptTask(task:)) }, { URLSessionSwizzler.unbind() }, - { try? URLSessionSwizzler.bind() }, - { URLSessionSwizzler.unbind() } ], iterations: 50 ) // swiftlint:enable opening_brace trailing_closure } + + func interceptRequest(request: URLRequest) -> URLRequest { + return request + } + + func interceptTask(task: URLSessionTask) { + } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskDelegateSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskDelegateSwizzlerTests.swift new file mode 100644 index 0000000000..63905788b0 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskDelegateSwizzlerTests.swift @@ -0,0 +1,107 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogInternal + +final class URLSessionTaskDelegateSwizzlerTests: XCTestCase { + override func tearDown() { + URLSessionTaskDelegateSwizzler.unbind(delegateClass: MockDelegate.self) + XCTAssertEqual(URLSessionTaskDelegateSwizzler.didFinishCollectingMap.count, 0) + + super.tearDown() + } + + func testSwizzling_whenMethodsAreImplemented() throws { + class MockDelegate: NSObject, URLSessionTaskDelegate { + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + } + } + + let delegate = MockDelegate() + let didFinishCollecting = XCTestExpectation(description: "didFinishCollecting") + + try URLSessionTaskDelegateSwizzler.bind( + delegateClass: MockDelegate.self, + interceptDidFinishCollecting: { _, _, _ in + didFinishCollecting.fulfill() + }, interceptDidCompleteWithError: { _, _, _ in + didFinishCollecting.fulfill() + } + ) + + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let task = session.dataTask(with: URL(string: "https://www.datadoghq.com/")!) + task.resume() + + wait(for: [didFinishCollecting], timeout: 5) + } + + func testSwizzling_whenMethodsAreNotImplemented() throws { + class MockDelegate: NSObject, URLSessionTaskDelegate { + } + + let delegate = MockDelegate() + let didFinishCollecting = XCTestExpectation(description: "didFinishCollecting") + + try URLSessionTaskDelegateSwizzler.bind( + delegateClass: MockDelegate.self, + interceptDidFinishCollecting: { _, _, _ in + didFinishCollecting.fulfill() + }, interceptDidCompleteWithError: { _, _, _ in + didFinishCollecting.fulfill() + } + ) + + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let task = session.dataTask(with: URL(string: "https://www.datadoghq.com/")!) + task.resume() + + wait(for: [didFinishCollecting], timeout: 5) + } + + func testBindings() throws { + XCTAssertNil(URLSessionTaskDelegateSwizzler.didFinishCollectingMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + + try URLSessionTaskDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidFinishCollecting: interceptDidFinishCollecting, interceptDidCompleteWithError: interceptDidCompleteWithError) + XCTAssertNotNil(URLSessionTaskDelegateSwizzler.didFinishCollectingMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + XCTAssertNotNil(URLSessionTaskDelegateSwizzler.didCompleteWithErrorMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + + try URLSessionTaskDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidFinishCollecting: interceptDidFinishCollecting, interceptDidCompleteWithError: interceptDidCompleteWithError) + XCTAssertNotNil(URLSessionTaskDelegateSwizzler.didFinishCollectingMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + XCTAssertNotNil(URLSessionTaskDelegateSwizzler.didCompleteWithErrorMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + + URLSessionTaskDelegateSwizzler.unbind(delegateClass: MockDelegate.self) + XCTAssertNil(URLSessionTaskDelegateSwizzler.didFinishCollectingMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + XCTAssertNil(URLSessionTaskDelegateSwizzler.didCompleteWithErrorMap[MetaTypeExtensions.key(from: MockDelegate.self)] as Any?) + } + + func testConcurrentBinding() throws { + // swiftlint:disable opening_brace trailing_closure + callConcurrently( + closures: [ + { try? URLSessionTaskDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidFinishCollecting: self.intercept(session:task:metrics:), interceptDidCompleteWithError: self.interceptDidCompleteWithError(session:task:error:)) }, + { URLSessionTaskDelegateSwizzler.unbind(delegateClass: MockDelegate.self) }, + { try? URLSessionTaskDelegateSwizzler.bind(delegateClass: MockDelegate.self, interceptDidFinishCollecting: self.intercept(session:task:metrics:), interceptDidCompleteWithError: self.interceptDidCompleteWithError(session:task:error:)) }, + { URLSessionTaskDelegateSwizzler.unbind(delegateClass: MockDelegate.self) }, + ], + iterations: 50 + ) + // swiftlint:enable opening_brace trailing_closure + } + + func intercept(session: URLSession, task: URLSessionTask, metrics: URLSessionTaskMetrics) { + } + + func interceptDidFinishCollecting(session: URLSession, task: URLSessionTask, metrics: URLSessionTaskMetrics) { + } + + func interceptDidCompleteWithError(session: URLSession, task: URLSessionTask, error: Error?) { + } + + class MockDelegate: NSObject, URLSessionTaskDelegate { + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskSwizzlerTests.swift new file mode 100644 index 0000000000..2728648207 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskSwizzlerTests.swift @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogInternal + +final class URLSessionTaskSwizzlerTests: XCTestCase { + override func tearDown() { + URLSessionTaskSwizzler.unbind() + XCTAssertNil(URLSessionTaskSwizzler.resume as Any?) + + super.tearDown() + } + + func testSwizzling() throws { + let expectation = XCTestExpectation(description: "resume") + try URLSessionTaskSwizzler.bind { _ in + expectation.fulfill() + } + + let session = URLSession(configuration: .default) + let task = session.dataTask(with: URL(string: "https://www.datadoghq.com/")!) + task.resume() + + wait(for: [expectation], timeout: 5) + } + + func testBindings() { + XCTAssertNil(URLSessionTaskSwizzler.resume as Any?) + + try? URLSessionTaskSwizzler.bind(interceptResume: { _ in }) + XCTAssertNotNil(URLSessionTaskSwizzler.resume as Any?) + + try? URLSessionTaskSwizzler.bind(interceptResume: { _ in }) + XCTAssertNotNil(URLSessionTaskSwizzler.resume as Any?) + + URLSessionTaskSwizzler.unbind() + XCTAssertNil(URLSessionTaskSwizzler.resume as Any?) + } + + func testConcurrentBinding() throws { + // swiftlint:disable opening_brace trailing_closure + callConcurrently( + closures: [ + { try? URLSessionTaskSwizzler.bind(interceptResume: self.intercept(task:)) }, + { URLSessionTaskSwizzler.unbind() }, + { try? URLSessionTaskSwizzler.bind(interceptResume: self.intercept(task:)) }, + { URLSessionTaskSwizzler.unbind() }, + ], + iterations: 50 + ) + // swiftlint:enable opening_brace trailing_closure + } + + func intercept(task: URLSessionTask) { + } +} diff --git a/DatadogInternal/Tests/Utils/MetaTypeExtensionsTests.swift b/DatadogInternal/Tests/Utils/MetaTypeExtensionsTests.swift new file mode 100644 index 0000000000..de9c473ba8 --- /dev/null +++ b/DatadogInternal/Tests/Utils/MetaTypeExtensionsTests.swift @@ -0,0 +1,52 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import DatadogInternal + +final class MetaTypeExtensionsTests: XCTestCase { + func testKey() throws { + // Prefix depends on the scheme which may be iOS or tvOS, hence only assert suffix. + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelStruct.self).hasSuffix(".TopLevelStruct")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelStruct.NestedStructInStruct.self).hasSuffix(".TopLevelStruct.NestedStructInStruct")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelStruct.NestedClassInStruct.self).hasSuffix(".TopLevelStruct.NestedClassInStruct")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelStruct.NestedEnumInStruct.self).hasSuffix(".TopLevelStruct.NestedEnumInStruct")) + + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelClass.self).hasSuffix(".TopLevelClass")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelClass.NestedStructInClass.self).hasSuffix(".TopLevelClass.NestedStructInClass")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelClass.NestedClassInClass.self).hasSuffix(".TopLevelClass.NestedClassInClass")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelClass.NestedEnumInClass.self).hasSuffix(".TopLevelClass.NestedEnumInClass")) + + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelEnum.self).hasSuffix(".TopLevelEnum")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelEnum.NestedStructInEnum.self).hasSuffix(".TopLevelEnum.NestedStructInEnum")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelEnum.NestedClassInEnum.self).hasSuffix(".TopLevelEnum.NestedClassInEnum")) + XCTAssertTrue(MetaTypeExtensions.key(from: TopLevelEnum.NestedEnumInEnum.self).hasSuffix(".TopLevelEnum.NestedEnumInEnum")) + } +} + +struct TopLevelStruct { + struct NestedStructInStruct {} + + class NestedClassInStruct {} + + enum NestedEnumInStruct {} +} + +class TopLevelClass { + struct NestedStructInClass {} + + class NestedClassInClass {} + + enum NestedEnumInClass {} +} + +enum TopLevelEnum { + struct NestedStructInEnum {} + + class NestedClassInEnum {} + + enum NestedEnumInEnum {} +} diff --git a/DatadogObjc/Sources/DDURLSessionInstrumentation+objc.swift b/DatadogObjc/Sources/DDURLSessionInstrumentation+objc.swift new file mode 100644 index 0000000000..7725e92afd --- /dev/null +++ b/DatadogObjc/Sources/DDURLSessionInstrumentation+objc.swift @@ -0,0 +1,76 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogCore +import DatadogInternal + +/// Configuration of URLSession instrumentation. +@objc +public class DDURLSessionInstrumentationConfiguration: NSObject { + internal var swiftConfig: URLSessionInstrumentation.Configuration + + @objc + public init(delegateClass: URLSessionDataDelegate.Type) { + swiftConfig = .init(delegateClass: delegateClass) + } + + /// Sets additional first party hosts to consider in the interception. + @objc + public func setFirstPartyHostsTracing(_ firstPartyHostsTracing: DDURLSessionInstrumentationFirstPartyHostsTracing) { + swiftConfig.firstPartyHostsTracing = firstPartyHostsTracing.swiftType + } + + /// The delegate class to be used to swizzle URLSessionTaskDelegate & URLSessionDataDelegate methods. + @objc public var delegateClass: URLSessionDataDelegate.Type { + set { swiftConfig.delegateClass = newValue } + get { swiftConfig.delegateClass } + } +} + +/// Defines configuration for first-party hosts in distributed tracing. +@objc +public class DDURLSessionInstrumentationFirstPartyHostsTracing: NSObject { + internal var swiftType: URLSessionInstrumentation.FirstPartyHostsTracing + + @objc + public init(hostsWithHeaderTypes: [String: Set]) { + let swiftHostsWithHeaders = hostsWithHeaderTypes.mapValues { headerTypes in + Set(headerTypes.map { + $0.swiftType + }) + } + swiftType = .traceWithHeaders(hostsWithHeaders: swiftHostsWithHeaders) + } + + @objc + public init(hosts: Set) { + swiftType = .trace(hosts: hosts) + } +} + +@objc +public class DDURLSessionInstrumentation: NSObject { + /// Enables URLSession instrumentation. + /// + /// - Parameters: + /// - configuration: Configuration of the feature. + @objc + public static func enable(configuration: DDURLSessionInstrumentationConfiguration) { + URLSessionInstrumentation.enable(with: configuration.swiftConfig) + } + + /// Disables URLSession instrumentation. + /// - Parameters: + /// - delegateClass: The delegate class to unbind. + @objc + public static func disable(delegateClass: URLSessionDataDelegate.Type) { + if delegateClass == DDNSURLSessionDelegate.self { + URLSessionInstrumentation.disable(delegateClass: DatadogURLSessionDelegate.self) + } + URLSessionInstrumentation.disable(delegateClass: delegateClass) + } +} diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index 34e7784e66..c64dd1e316 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -12,6 +12,7 @@ import DatadogInternal @_exported import class DatadogInternal.DatadogURLSessionDelegate @_exported import typealias DatadogInternal.DDURLSessionDelegate @_exported import protocol DatadogInternal.__URLSessionDelegateProviding +@_exported import enum DatadogInternal.URLSessionInstrumentation // swiftlint:enable duplicate_imports extension RUM { diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 491af50eec..9f840a54f3 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -105,7 +105,7 @@ class RUMInstrumentationTests: XCTestCase { internal func DDAssertActiveSwizzlings(_ expectedSwizzledSelectors: [String], file: StaticString = #filePath, line: UInt = #line) { _DDEvaluateAssertion(message: "Only \(expectedSwizzledSelectors) swizzlings should be active", file: file, line: line) { - let actual = activeSwizzlingNames.sorted() + let actual = Swizzling.activeSwizzlingNames.sorted() let expected = expectedSwizzledSelectors.sorted() guard actual == expected else { diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/UIApplicationSwizzler.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/UIApplicationSwizzler.swift index 1250236d64..c3568155cc 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/UIApplicationSwizzler.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/UIApplicationSwizzler.swift @@ -6,6 +6,7 @@ #if os(iOS) import UIKit +import DatadogInternal // MARK: - Copy & Paste from Datadog SDK @@ -122,7 +123,7 @@ internal class MethodSwizzler { set(newIMP: newImp, for: foundMethod) #if DD_SDK_COMPILED_FOR_TESTING - activeSwizzlingNames.append(foundMethod.swizzlingName) + Swizzling.activeSwizzlingNames.append(foundMethod.swizzlingName) #endif } } @@ -134,7 +135,7 @@ internal class MethodSwizzler { let originalIMP: IMP = unsafeBitCast(originalTypedIMP, to: IMP.self) method_setImplementation(foundMethod.method, originalIMP) - activeSwizzlingNames.removeAll { $0 == foundMethod.swizzlingName } + Swizzling.activeSwizzlingNames.removeAll { $0 == foundMethod.swizzlingName } } } @@ -176,7 +177,4 @@ internal class MethodSwizzler { internal extension MethodSwizzler.FoundMethod { var swizzlingName: String { "\(klass).\(method_getName(method))" } } - -/// The list of active swizzlings to ensure integrity in unit tests. -internal var activeSwizzlingNames: [String] = [] #endif diff --git a/DatadogTrace/Sources/TraceConfiguration.swift b/DatadogTrace/Sources/TraceConfiguration.swift index 21c7d58414..b41cf2a6ef 100644 --- a/DatadogTrace/Sources/TraceConfiguration.swift +++ b/DatadogTrace/Sources/TraceConfiguration.swift @@ -12,6 +12,7 @@ import DatadogInternal @_exported import class DatadogInternal.DatadogURLSessionDelegate @_exported import typealias DatadogInternal.DDURLSessionDelegate @_exported import protocol DatadogInternal.__URLSessionDelegateProviding +@_exported import enum DatadogInternal.URLSessionInstrumentation @_exported import class DatadogInternal.HTTPHeadersWriter @_exported import class DatadogInternal.B3HTTPHeadersWriter diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift index 7cd771bc4f..aba62bf078 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift @@ -19,7 +19,7 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { let expectedThirdPartyRequestsViewControllerName: String } - func testRUMURLSessionResourcesScenario() throws { + func testRUMURLSessionResourcesScenario_composition() throws { try runTest( for: "RUMURLSessionResourcesScenario", expectations: Expectations( @@ -27,13 +27,55 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { expectedThirdPartyRequestsViewControllerName: "Runner.SendThirdPartyRequestsViewController" ), urlSessionSetup: .init( - instrumentationMethod: .allCases.randomElement()!, - initializationMethod: .allCases.randomElement()! + instrumentationMethod: .composition, + initializationMethod: .afterSDK + ) + ) + } + + func testRUMURLSessionResourcesScenario_directWithAdditionalFirstyPartyHosts() throws { + try runTest( + for: "RUMURLSessionResourcesScenario", + expectations: Expectations( + expectedFirstPartyRequestsViewControllerName: "Runner.SendFirstPartyRequestsViewController", + expectedThirdPartyRequestsViewControllerName: "Runner.SendThirdPartyRequestsViewController" + ), + urlSessionSetup: .init( + instrumentationMethod: .directWithAdditionalFirstyPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testRUMURLSessionResourcesScenario_directWithGlobalFirstPartyHosts() throws { + try runTest( + for: "RUMURLSessionResourcesScenario", + expectations: Expectations( + expectedFirstPartyRequestsViewControllerName: "Runner.SendFirstPartyRequestsViewController", + expectedThirdPartyRequestsViewControllerName: "Runner.SendThirdPartyRequestsViewController" + ), + urlSessionSetup: .init( + instrumentationMethod: .directWithGlobalFirstPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testRUMURLSessionResourcesScenario_inheritance() throws { + try runTest( + for: "RUMURLSessionResourcesScenario", + expectations: Expectations( + expectedFirstPartyRequestsViewControllerName: "Runner.SendFirstPartyRequestsViewController", + expectedThirdPartyRequestsViewControllerName: "Runner.SendThirdPartyRequestsViewController" + ), + urlSessionSetup: .init( + instrumentationMethod: .inheritance, + initializationMethod: .afterSDK ) ) } - func testRUMNSURLSessionResourcesScenario() throws { + func testRUMNSURLSessionResourcesScenario_composition() throws { try runTest( for: "RUMNSURLSessionResourcesScenario", expectations: Expectations( @@ -41,8 +83,50 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { expectedThirdPartyRequestsViewControllerName: "ObjcSendThirdPartyRequestsViewController" ), urlSessionSetup: .init( - instrumentationMethod: .allCases.randomElement()!, - initializationMethod: .allCases.randomElement()! + instrumentationMethod: .composition, + initializationMethod: .afterSDK + ) + ) + } + + func testRUMNSURLSessionResourcesScenario_directWithAdditionalFirstyPartyHosts() throws { + try runTest( + for: "RUMNSURLSessionResourcesScenario", + expectations: Expectations( + expectedFirstPartyRequestsViewControllerName: "ObjcSendFirstPartyRequestsViewController", + expectedThirdPartyRequestsViewControllerName: "ObjcSendThirdPartyRequestsViewController" + ), + urlSessionSetup: .init( + instrumentationMethod: .directWithAdditionalFirstyPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testRUMNSURLSessionResourcesScenario_directWithGlobalFirstPartyHosts() throws { + try runTest( + for: "RUMNSURLSessionResourcesScenario", + expectations: Expectations( + expectedFirstPartyRequestsViewControllerName: "ObjcSendFirstPartyRequestsViewController", + expectedThirdPartyRequestsViewControllerName: "ObjcSendThirdPartyRequestsViewController" + ), + urlSessionSetup: .init( + instrumentationMethod: .directWithGlobalFirstPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testRUMNSURLSessionResourcesScenario_inheritance() throws { + try runTest( + for: "RUMNSURLSessionResourcesScenario", + expectations: Expectations( + expectedFirstPartyRequestsViewControllerName: "ObjcSendFirstPartyRequestsViewController", + expectedThirdPartyRequestsViewControllerName: "ObjcSendThirdPartyRequestsViewController" + ), + urlSessionSetup: .init( + instrumentationMethod: .inheritance, + initializationMethod: .afterSDK ) ) } @@ -50,6 +134,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { /// Both, `URLSession` (Swift) and `NSURLSession` (Objective-C) scenarios use different storyboards /// and different view controllers to run this test, but the the logic and the instrumentation is the same. private func runTest(for testScenarioClassName: String, expectations: Expectations, urlSessionSetup: URLSessionSetup) throws { + precondition(urlSessionSetup.initializationMethod == .afterSDK, "The SDK must be initialized before enabling URLSession ") + // Server session recording first party requests send to `HTTPServerMock`. // Used to assert that trace propagation headers are send for first party requests. let customFirstPartyServerSession = server.obtainUniqueRecordingSession() @@ -144,9 +230,9 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { XCTAssertNotNil(firstPartyResource1.resource.duration) XCTAssertGreaterThan(firstPartyResource1.resource.duration!, 0) - XCTAssertNil(firstPartyResource1.dd.traceId, "`firstPartyGETResourceURL` should not be traced") - XCTAssertNil(firstPartyResource1.dd.spanId, "`firstPartyGETResourceURL` should not be traced") - XCTAssertNil(firstPartyResource1.dd.rulePsr, "Not traced resource should not send sample rate") + XCTAssertNotNil(firstPartyResource1.dd.traceId) + XCTAssertNotNil(firstPartyResource1.dd.spanId) + XCTAssertNotNil(firstPartyResource1.dd.rulePsr) let firstPartyResource2 = try XCTUnwrap( session.viewVisits[0].resourceEvents.first { $0.resource.url == firstPartyPOSTResourceURL.absoluteString }, diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift index b01044c89d..ab791b66ef 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift @@ -14,22 +14,72 @@ private extension ExampleApplication { } class TracingURLSessionScenarioTests: IntegrationTests, TracingCommonAsserts { - func testTracingURLSessionScenario() throws { + func testTracingURLSessionScenario_composition() throws { try runTest( for: "TracingURLSessionScenario", urlSessionSetup: .init( - instrumentationMethod: .allCases.randomElement()!, - initializationMethod: .allCases.randomElement()! + instrumentationMethod: .composition, + initializationMethod: .afterSDK + ) + ) + } + + func testTracingURLSessionScenario_directWithAdditionalFirstyPartyHosts() throws { + try runTest( + for: "TracingURLSessionScenario", + urlSessionSetup: .init( + instrumentationMethod: .directWithAdditionalFirstyPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testTracingURLSessionScenario_directWithGlobalFirstPartyHosts() throws { + try runTest( + for: "TracingURLSessionScenario", + urlSessionSetup: .init( + instrumentationMethod: .directWithGlobalFirstPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testTracingURLSessionScenario_inheritance() throws { + try runTest( + for: "TracingURLSessionScenario", + urlSessionSetup: .init( + instrumentationMethod: .inheritance, + initializationMethod: .afterSDK ) ) } - func testTracingNSURLSessionScenario() throws { + func testTracingNSURLSessionScenario_composition() throws { + try runTest( + for: "TracingNSURLSessionScenario", + urlSessionSetup: .init( + instrumentationMethod: .composition, + initializationMethod: .afterSDK + ) + ) + } + + func testTracingNSURLSessionScenario_directWithAdditionalFirstyPartyHosts() throws { + try runTest( + for: "TracingNSURLSessionScenario", + urlSessionSetup: .init( + instrumentationMethod: .directWithAdditionalFirstyPartyHosts, + initializationMethod: .afterSDK + ) + ) + } + + func testTracingNSURLSessionScenario_inheritance() throws { try runTest( for: "TracingNSURLSessionScenario", urlSessionSetup: .init( - instrumentationMethod: .allCases.randomElement()!, - initializationMethod: .allCases.randomElement()! + instrumentationMethod: .inheritance, + initializationMethod: .afterSDK ) ) } diff --git a/IntegrationTests/Runner/Scenarios/RUM/RUMScenarios.swift b/IntegrationTests/Runner/Scenarios/RUM/RUMScenarios.swift index aa7cdfa732..57a14cf67f 100644 --- a/IntegrationTests/Runner/Scenarios/RUM/RUMScenarios.swift +++ b/IntegrationTests/Runner/Scenarios/RUM/RUMScenarios.swift @@ -344,11 +344,10 @@ private func rumResourceAttributesProvider( if let responseHeaders = (response as? HTTPURLResponse)?.allHeaderFields { responseHeadersValue = format(headers: responseHeaders) } - if let data = data { - responseBodyValue = String(data: data, encoding: .utf8) ?? "" - } if let error = error { errorDetailsValue = String(describing: error) + } else { + responseBodyValue = String(data: data ?? .init(), encoding: .utf8) ?? "" } return [ diff --git a/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSPictureViewController.swift b/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSPictureViewController.swift index 9dbe88afdf..8552ed6db0 100644 --- a/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSPictureViewController.swift +++ b/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSPictureViewController.swift @@ -7,9 +7,10 @@ import UIKit internal class TSPictureViewController: UIViewController { + let sessionDelegate = DDURLSessionDelegate() private lazy var session = URLSession( configuration: .default, - delegate: DDURLSessionDelegate(), + delegate: sessionDelegate, delegateQueue: nil ) diff --git a/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift b/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift index 05aee40d25..c3af654fc1 100644 --- a/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift +++ b/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift @@ -20,22 +20,18 @@ private class InheritedURLSessionDelegate: DDURLSessionDelegate { /// An example of instrumenting existing `URLSessionDelegate` with `DDURLSessionDelegate` through composition. private class CompositedURLSessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate, __URLSessionDelegateProviding { // MARK: - __URLSessionDelegateProviding conformance - let ddURLSessionDelegate: DatadogURLSessionDelegate = DDURLSessionDelegate() // MARK: - __URLSessionDelegateProviding handling func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - ddURLSessionDelegate.urlSession(session, task: task, didFinishCollecting: metrics) // forward to DD /* run custom logic */ } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - ddURLSessionDelegate.urlSession(session, task: task, didCompleteWithError: error) // forward to DD /* run custom logic */ } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - ddURLSessionDelegate.urlSession(session, dataTask: dataTask, didReceive: data) // forward to DD /* run custom logic */ } } @@ -142,7 +138,7 @@ class URLSessionBaseScenario: NSObject { } private func createInstrumentedURLSession() -> URLSession { - let delegate: URLSessionDelegate + let delegate: URLSessionDataDelegate switch setup.instrumentationMethod { case .directWithGlobalFirstPartyHosts: @@ -159,6 +155,7 @@ class URLSessionBaseScenario: NSObject { delegate = InheritedURLSessionDelegate() case .composition: delegate = CompositedURLSessionDelegate() + URLSessionInstrumentation.enable(with: .init(delegateClass: CompositedURLSessionDelegate.self)) } return URLSession( diff --git a/TestUtilities/Helpers/XCTestCase.swift b/TestUtilities/Helpers/XCTestCase.swift index 1e183e46ce..40ebd9d97f 100644 --- a/TestUtilities/Helpers/XCTestCase.swift +++ b/TestUtilities/Helpers/XCTestCase.swift @@ -47,4 +47,20 @@ extension XCTestCase { } } } + + @available(iOS 13.0, tvOS 13.0, *) + public func dd_fulfillment( + for expectations: [XCTestExpectation], + timeout seconds: TimeInterval = .infinity, + enforceOrder enforceOrderOfFulfillment: Bool = false) async { +#if compiler(>=5.8) + await fulfillment(of: expectations, timeout: seconds, enforceOrder: enforceOrderOfFulfillment) +#else + wait( + for: expectations, + timeout: seconds, + enforceOrder: enforceOrderOfFulfillment + ) +#endif + } } diff --git a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift index 75c8436bde..95a4baeba1 100644 --- a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift +++ b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift @@ -53,8 +53,8 @@ public final class URLSessionHandlerMock: DatadogURLSessionHandler { public var onRequestMutation: ((URLRequest, Set) -> Void)? public var onRequestInterception: ((URLRequest) -> Void)? - public var onInterceptionStart: ((URLSessionTaskInterception) -> Void)? - public var onInterceptionComplete: ((URLSessionTaskInterception) -> Void)? + public var onInterceptionDidStart: ((URLSessionTaskInterception) -> Void)? + public var onInterceptionDidComplete: ((URLSessionTaskInterception) -> Void)? @ReadWriteLock public private(set) var interceptions: [UUID: URLSessionTaskInterception] = [:] @@ -77,12 +77,12 @@ public final class URLSessionHandlerMock: DatadogURLSessionHandler { } public func interceptionDidStart(interception: URLSessionTaskInterception) { - onInterceptionStart?(interception) + onInterceptionDidStart?(interception) interceptions[interception.identifier] = interception } public func interceptionDidComplete(interception: URLSessionTaskInterception) { - onInterceptionComplete?(interception) + onInterceptionDidComplete?(interception) interceptions[interception.identifier] = interception } } diff --git a/TestUtilities/Mocks/ServerMock.swift b/TestUtilities/Mocks/ServerMock.swift index 888d9c911a..5c46271916 100644 --- a/TestUtilities/Mocks/ServerMock.swift +++ b/TestUtilities/Mocks/ServerMock.swift @@ -91,6 +91,7 @@ public class ServerMock { /// An unique identifier of the `URLSession` produced by this instance of `ServerMock`. public let urlSessionUUID = UUID() private let queue: DispatchQueue + private let skipIsMainThreadCheck: Bool fileprivate let mockedResponse: HTTPURLResponse? fileprivate let mockedData: Data? @@ -101,7 +102,7 @@ public class ServerMock { case failure(error: NSError) } - public init(delivery: Delivery) { + public init(delivery: Delivery, skipIsMainThreadCheck: Bool = false) { switch delivery { case let .success(response: response, data: data): self.mockedResponse = response @@ -112,7 +113,8 @@ public class ServerMock { self.mockedData = nil self.mockedError = error } - precondition(Thread.isMainThread, "`ServerMock` should be initialized on the main thread.") + self.skipIsMainThreadCheck = skipIsMainThreadCheck + precondition(skipIsMainThreadCheck || Thread.isMainThread, "`ServerMock` should be initialized on the main thread.") precondition(ServerMock.activeInstance == nil, "Only one active instance of `ServerMock` is allowed at a time.") self.queue = DispatchQueue(label: "com.datadoghq.ServerMock-\(urlSessionUUID.uuidString)") @@ -136,7 +138,7 @@ public class ServerMock { /// /// NOTE: one of the `wait*` methods **must be called** within the test using `ServerMock`. /// - precondition(Thread.isMainThread, "`ServerMock` should be deinitialized on the main thread.") + precondition(Thread.isMainThread || skipIsMainThreadCheck, "`ServerMock` should be deinitialized on the main thread.") } fileprivate func record(newRequest: URLRequest) { @@ -218,7 +220,7 @@ public class ServerMock { _ = waitAndReturnRequests(count: requestsCount, timeout: timeout) } - /// Waits an arbitrary amount of time and asserts that no requests were sent to `ServerMock`. + /// Waits an arbitrary amount of time and asserts that no requests were sent to `ServerMock`. public func waitAndAssertNoRequestsSent(file: StaticString = #file, line: UInt = #line) { waitFor(requestsCompletion: 0) }