diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe8fd890f..fd5b59ce0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ # Unreleased +- [IMPROVEMENT] Start sending data immediately after SDK is initialized. See [#1798][] +- [FEATURE] `DatadogTrace` now supports head-based sampling. See [#1794][] +- [FEATURE] Support WebView recording in Session Replay. See [#1776][] +- [IMPROVEMENT] Add `isInitialized` and `stopInstance` methods to ObjC API. See [#1800][] +- [IMPROVEMENT] Add `addUserExtraInfo` method to ObjC API. See [#1799][] +- [FIX] Use trace and span id as decimal. See [#1807][] + # 2.10.0 / 23-04-2024 - [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] - [FEATURE] Add support for 128 bit trace IDs. See [#1721][] - [FEATURE] Fatal App Hangs are tracked in RUM. See [#1763][] - [FIX] Avoid name collision with Required Reason APIs. See [#1774][] +- [IMPROVEMENT] Make the SDK compile on macOS 12+. See [#1711][] # 2.9.0 / 11-04-2024 @@ -629,16 +637,21 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1696]: https://github.com/DataDog/dd-sdk-ios/pull/1696 [#1697]: https://github.com/DataDog/dd-sdk-ios/pull/1697 [#1707]: https://github.com/DataDog/dd-sdk-ios/pull/1707 +[#1711]: https://github.com/DataDog/dd-sdk-ios/pull/1711 +[#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 [#1722]: https://github.com/DataDog/dd-sdk-ios/pull/1722 [#1724]: https://github.com/DataDog/dd-sdk-ios/pull/1724 [#1741]: https://github.com/DataDog/dd-sdk-ios/pull/1741 [#1742]: https://github.com/DataDog/dd-sdk-ios/pull/1742 [#1746]: https://github.com/DataDog/dd-sdk-ios/pull/1746 +[#1747]: https://github.com/DataDog/dd-sdk-ios/pull/1747 +[#1794]: https://github.com/DataDog/dd-sdk-ios/pull/1794 [#1774]: https://github.com/DataDog/dd-sdk-ios/pull/1774 [#1763]: https://github.com/DataDog/dd-sdk-ios/pull/1763 [#1767]: https://github.com/DataDog/dd-sdk-ios/pull/1767 -[#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 -[#1747]: https://github.com/DataDog/dd-sdk-ios/pull/1747 +[#1798]: https://github.com/DataDog/dd-sdk-ios/pull/1798 +[#1776]: https://github.com/DataDog/dd-sdk-ios/pull/1776 +[#1807]: https://github.com/DataDog/dd-sdk-ios/pull/1807 [@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 1da0f2959a..3a2ed98c4b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 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, ); }; }; + 3C2B89422BE53D6200043847 /* RUMContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2B89412BE53D6200043847 /* RUMContextMocks.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 */; }; @@ -300,6 +301,10 @@ 614CADD72510BAC000B93D2D /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614CADD62510BAC000B93D2D /* Environment.swift */; }; 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614E9EB2244719FA007EE3E1 /* BundleType.swift */; }; 614ED36C260352DC00C8C519 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */; }; + 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */; }; + 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */; }; + 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */; }; 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Trace+objc.swift */; }; 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; @@ -352,6 +357,8 @@ 6167E72A2B84C11900C3CA2D /* DDCrashReportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */; }; 6167E72C2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */; }; 6167E72D2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */; }; + 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */; }; + 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */; }; 616B668E259CC28E00968EE8 /* DDRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */; }; 616F8C272BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */; }; 616F8C282BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */; }; @@ -361,6 +368,8 @@ 617247B825DAB0E2007085B3 /* DDCrashReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */; }; 6175922B2A6FA8EE0073F431 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; 6175922D2A6FADDD0073F431 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; + 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */; }; + 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */; }; 617699182A860D9D0030022B /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699172A860D9D0030022B /* HTTPClient.swift */; }; 617699192A860D9D0030022B /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699172A860D9D0030022B /* HTTPClient.swift */; }; 6176991B2A86121B0030022B /* HTTPClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176991A2A86121B0030022B /* HTTPClientMock.swift */; }; @@ -595,6 +604,7 @@ A7F651302B7655DE004B0EDB /* UIImageResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */; }; A7FA98CE2BA1A6930018D6B5 /* SharedMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* SharedMetrics.swift */; }; A7FA98CF2BA1A6930018D6B5 /* SharedMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* SharedMetrics.swift */; }; + D2056C212BBFE05A0085BC76 /* WireframesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */; }; D20605A3287464F40047275C /* ContextValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A2287464F40047275C /* ContextValuePublisher.swift */; }; D20605A4287464F40047275C /* ContextValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A2287464F40047275C /* ContextValuePublisher.swift */; }; D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A5287476230047275C /* ServerOffsetPublisher.swift */; }; @@ -1004,6 +1014,7 @@ D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; + D27CBD9A2BB5DBBB00C766AA /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27CBD992BB5DBBB00C766AA /* Mocks.swift */; }; D27D81C12A5D415200281CC2 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; D27D81C22A5D415200281CC2 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; D27D81C32A5D415200281CC2 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; @@ -1285,6 +1296,9 @@ D2C1A57429C4F30000946C31 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; D2C5D5282B83FD5300B63F36 /* WebViewMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AE740F2AD6EE4E008DB9BB /* WebViewMessageTests.swift */; }; D2C5D5292B83FD5400B63F36 /* WebViewMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AE740F2AD6EE4E008DB9BB /* WebViewMessageTests.swift */; }; + D2C5D52B2B84F6AB00B63F36 /* WebViewRecordReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */; }; + D2C5D52D2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */; }; + D2C5D5302B84F71200B63F36 /* WebRecordIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */; }; D2C7E3AB28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */; }; D2C7E3AE28FEBDA10023B2CC /* LaunchTimePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */; }; D2CB6E0C27C50EAE00A62B57 /* DatadogCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133B85242393DE00786299 /* DatadogCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1968,6 +1982,7 @@ 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 = ""; }; + 3C2B89412BE53D6200043847 /* RUMContextMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextMocks.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 = ""; }; 3C9B27242B9F174700569C07 /* SpanID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanID.swift; sourceTree = ""; }; @@ -2242,6 +2257,8 @@ 614CADD62510BAC000B93D2D /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 614E9EB2244719FA007EE3E1 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CrashReporter.xcframework; path = ../Carthage/Build/CrashReporter.xcframework; sourceTree = ""; }; + 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersWriterTests.swift; sourceTree = ""; }; + 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatadogTracer+InjectAndExtract.swift"; sourceTree = ""; }; 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MockServerAddress.local.xcconfig; sourceTree = ""; }; 615519252461BCE7002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 615519262461BCE7002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; @@ -2286,6 +2303,7 @@ 6167E7222B837FF100C3CA2D /* BinaryImageMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryImageMocks.swift; sourceTree = ""; }; 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportMocks.swift; sourceTree = ""; }; 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitHelpers.swift; sourceTree = ""; }; + 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TraceSamplingStrategy+objc.swift"; sourceTree = ""; }; 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMMonitorTests.swift; sourceTree = ""; }; 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfo.swift; sourceTree = ""; }; 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfoTests.swift; sourceTree = ""; }; @@ -2299,6 +2317,7 @@ 617247AD25DA9BEA007085B3 /* CrashReportingObjcHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CrashReportingObjcHelpers.h; sourceTree = ""; }; 617247AE25DA9BEA007085B3 /* CrashReportingObjcHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashReportingObjcHelpers.m; sourceTree = ""; }; 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportBuilder.swift; sourceTree = ""; }; + 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceContext.swift; sourceTree = ""; }; 617699172A860D9D0030022B /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 6176991A2A86121B0030022B /* HTTPClientMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClientMock.swift; sourceTree = ""; }; 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datadog+MultipleInstancesIntegrationTests.swift"; sourceTree = ""; }; @@ -2570,6 +2589,7 @@ B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReaderTests.swift; sourceTree = ""; }; B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalInfo.swift; sourceTree = ""; }; B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoTests.swift; sourceTree = ""; }; + D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireframesBuilderTests.swift; sourceTree = ""; }; D20605A2287464F40047275C /* ContextValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValuePublisher.swift; sourceTree = ""; }; D20605A5287476230047275C /* ServerOffsetPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOffsetPublisher.swift; sourceTree = ""; }; D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoPublisher.swift; sourceTree = ""; }; @@ -2731,6 +2751,7 @@ D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = ""; }; D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = ""; }; + D27CBD992BB5DBBB00C766AA /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; D286626D2A43487500852CE3 /* Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Datadog.swift; sourceTree = ""; }; D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSpanTests.swift; sourceTree = ""; }; D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingURLSessionHandlerTests.swift; sourceTree = ""; }; @@ -2783,6 +2804,9 @@ D2BEEDB72B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskDelegateSwizzlerTests.swift; sourceTree = ""; }; D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogTrace.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2C1A57329C4F2E800946C31 /* DatadogTraceTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogTraceTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewRecordReceiver.swift; sourceTree = ""; }; + D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewRecordReceiverTests.swift; sourceTree = ""; }; + D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebRecordIntegrationTests.swift; sourceTree = ""; }; D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryStatusPublisherTests.swift; sourceTree = ""; }; D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimePublisher.swift; sourceTree = ""; }; D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -3217,6 +3241,7 @@ children = ( D297324F2A5C109A00827599 /* MessageEmitterTests.swift */, D29732502A5C109A00827599 /* WebViewTrackingTests.swift */, + D27CBD992BB5DBBB00C766AA /* Mocks.swift */, ); name = DatadogWebViewTrackingTests; path = ../DatadogWebViewTracking/Tests; @@ -3347,10 +3372,10 @@ 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */, 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */, 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */, - 61054E272A6EE10A00AAA894 /* NodeRecorders */, 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */, 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */, 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */, + 61054E272A6EE10A00AAA894 /* NodeRecorders */, ); path = ViewTreeSnapshot; sourceTree = ""; @@ -3385,6 +3410,7 @@ A73A54972B16406900E1F7E3 /* ResourcesFeature.swift */, 61054E3C2A6EE10A00AAA894 /* SessionReplayFeature.swift */, 61054E3E2A6EE10A00AAA894 /* RUMContextReceiver.swift */, + D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */, 61054E3F2A6EE10A00AAA894 /* SRContextPublisher.swift */, D22C5BCD2A98A65D0024CC1F /* Baggages.swift */, 61054E402A6EE10A00AAA894 /* RequestBuilders */, @@ -3426,7 +3452,7 @@ A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */, 61054E492A6EE10A00AAA894 /* Privacy */, 61054E4C2A6EE10A00AAA894 /* Diffing */, - 61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */, + 61054E4F2A6EE10A00AAA894 /* Builders */, 61054E522A6EE10A00AAA894 /* Flattening */, ); path = Processor; @@ -3449,13 +3475,13 @@ path = Diffing; sourceTree = ""; }; - 61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */ = { + 61054E4F2A6EE10A00AAA894 /* Builders */ = { isa = PBXGroup; children = ( 61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */, 61054E512A6EE10A00AAA894 /* WireframesBuilder.swift */, ); - path = SRDataModelsBuilder; + path = Builders; sourceTree = ""; }; 61054E522A6EE10A00AAA894 /* Flattening */ = { @@ -3526,7 +3552,7 @@ children = ( 61054F4F2A6EE1BA00AAA894 /* Privacy */, 61054F512A6EE1BA00AAA894 /* Diffing */, - 61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */, + 61054F542A6EE1BA00AAA894 /* Builders */, 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */, A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */, 61054F572A6EE1BA00AAA894 /* Flattening */, @@ -3551,12 +3577,13 @@ path = Diffing; sourceTree = ""; }; - 61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */ = { + 61054F542A6EE1BA00AAA894 /* Builders */ = { isa = PBXGroup; children = ( 61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */, + D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */, ); - path = SRDataModelsBuilder; + path = Builders; sourceTree = ""; }; 61054F572A6EE1BA00AAA894 /* Flattening */ = { @@ -3662,6 +3689,7 @@ 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */, 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */, 61054F862A6EE1BA00AAA894 /* SnapshotProducerMocks.swift */, + 3C2B89412BE53D6200043847 /* RUMContextMocks.swift */, 61054F872A6EE1BA00AAA894 /* RUMContextObserverMock.swift */, A74A72862B10CE4100771FEB /* ResourceMocks.swift */, A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */, @@ -3676,6 +3704,7 @@ children = ( 61054F892A6EE1BA00AAA894 /* RUMContextReceiverTests.swift */, 61054F8A2A6EE1BA00AAA894 /* SRContextPublisherTests.swift */, + D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */, 61054F8B2A6EE1BA00AAA894 /* RequestBuilder */, ); path = Feature; @@ -3719,6 +3748,7 @@ 610ABD492A69309900AFEA34 /* IntegrationUnitTests */ = { isa = PBXGroup; children = ( + D2C5D52E2B84F6E700B63F36 /* SessionReplay */, 6179DB542B60229D00E9E04E /* CrashReporting */, 61E8C5062B28896100E709B4 /* RUM */, 61F3E36B2BC7D51400C7881E /* Trace */, @@ -4249,6 +4279,7 @@ 6132BF4A24A49C7200D7BD17 /* Propagation */ = { isa = PBXGroup; children = ( + 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */, 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */, A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */, A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */, @@ -5732,6 +5763,7 @@ 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */, 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */, + 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */, 61C5A89824509C1100DA608C /* DDSpanTests.swift */, 61F1A620249A45E400075390 /* DDSpanContextTests.swift */, D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */, @@ -5971,6 +6003,14 @@ path = Models; sourceTree = ""; }; + D2C5D52E2B84F6E700B63F36 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; D2DA238B298D588A00C6C7E6 /* DatadogInternalTests */ = { isa = PBXGroup; children = ( @@ -6017,6 +6057,7 @@ children = ( D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */, D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */, + 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */, D2EBEDCC29B893D800B15732 /* TraceID.swift */, 3C9B27242B9F174700569C07 /* SpanID.swift */, D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */, @@ -6045,6 +6086,7 @@ 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */, 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */, 61F3E3652BC595F600C7881E /* HTTPHeadersReaderTests.swift */, + 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */, A79B0F5A292B7C06008742B3 /* B3HTTPHeadersWriterTests.swift */, A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */, A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */, @@ -7596,6 +7638,7 @@ files = ( D29732532A5C109A00827599 /* WebViewTrackingTests.swift in Sources */, D29732512A5C109A00827599 /* MessageEmitterTests.swift in Sources */, + D27CBD9A2BB5DBBB00C766AA /* Mocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7686,6 +7729,7 @@ 61A1A44929643254007909E7 /* DatadogCoreProxy.swift in Sources */, D2A1EE3B287EECC000D28DFB /* CarrierInfoPublisherTests.swift in Sources */, D22743D829DEB8B4001A7EF9 /* VitalInfoTests.swift in Sources */, + D2C5D5302B84F71200B63F36 /* WebRecordIntegrationTests.swift in Sources */, 61FF282824B8A31E000B3D9B /* RUMEventMatcher.swift in Sources */, D29A9FD829DDC686005C54A4 /* UIKitRUMViewsPredicateTests.swift in Sources */, D29294E3291D652C00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */, @@ -7852,6 +7896,7 @@ 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */, 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */, 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */, + 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */, 9E55407C25812D1C00F6E3AD /* RUM+objc.swift in Sources */, D2A434AA2A8E40A20028E329 /* SessionReplay+objc.swift in Sources */, @@ -7878,6 +7923,7 @@ 61054E812A6EE10A00AAA894 /* UIStepperRecorder.swift in Sources */, 61054E632A6EE10A00AAA894 /* SessionReplayConfiguration.swift in Sources */, 61054E702A6EE10A00AAA894 /* TouchSnapshotProducer.swift in Sources */, + D2C5D52B2B84F6AB00B63F36 /* WebViewRecordReceiver.swift in Sources */, 61054E652A6EE10A00AAA894 /* AppWindowObserver.swift in Sources */, A74A72812B0CEE4900771FEB /* ResourceRequestBuilder.swift in Sources */, 61054E7B2A6EE10A00AAA894 /* UIViewRecorder.swift in Sources */, @@ -7949,6 +7995,7 @@ 61054FB52A6EE1BA00AAA894 /* UISliderRecorderTests.swift in Sources */, 61054FB22A6EE1BA00AAA894 /* UILabelRecorderTests.swift in Sources */, 61054FCE2A6EE1BA00AAA894 /* RUMContextObserverMock.swift in Sources */, + D2C5D52D2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift in Sources */, 61054FBA2A6EE1BA00AAA894 /* UIImageViewRecorderTests.swift in Sources */, 61054FC02A6EE1BA00AAA894 /* UITextViewRecorderTests.swift in Sources */, 61054F9A2A6EE1BA00AAA894 /* CFType+SafetyTests.swift in Sources */, @@ -8010,9 +8057,11 @@ 61054FA52A6EE1BA00AAA894 /* RecordsBuilderTests.swift in Sources */, 61054FD02A6EE1BA00AAA894 /* SRContextPublisherTests.swift in Sources */, 61054F9B2A6EE1BA00AAA894 /* QueueTests.swift in Sources */, + D2056C212BBFE05A0085BC76 /* WireframesBuilderTests.swift in Sources */, 61054F992A6EE1BA00AAA894 /* ColorsTests.swift in Sources */, 61054FBF2A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift in Sources */, 61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift in Sources */, + 3C2B89422BE53D6200043847 /* RUMContextMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8215,6 +8264,7 @@ D2EBEE2A29BA160F00B15732 /* TracingHTTPHeaders.swift in Sources */, D21A94F22B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D23039EC298D5236001A1FA3 /* LaunchTime.swift in Sources */, + 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */, D23039EE298D5236001A1FA3 /* FeatureMessageReceiver.swift in Sources */, D23039DE298D5235001A1FA3 /* Writer.swift in Sources */, D23039FA298D5236001A1FA3 /* Telemetry.swift in Sources */, @@ -8556,6 +8606,7 @@ 61F3E3632BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */, 619CE7612A458D66005588CB /* TraceTests.swift in Sources */, + 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */, D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */, D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */, @@ -8759,6 +8810,7 @@ 61F3E3642BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */, 619CE7622A458D66005588CB /* TraceTests.swift in Sources */, + 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */, D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */, D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */, @@ -9014,6 +9066,7 @@ 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, D2CB6FA027C5217A00A62B57 /* Trace+objc.swift in Sources */, D2CB6FA127C5217A00A62B57 /* HTTPHeadersWriter+objc.swift in Sources */, + 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, D2CB6FA227C5217A00A62B57 /* DDSpan+objc.swift in Sources */, D2CB6FA327C5217A00A62B57 /* OTSpan+objc.swift in Sources */, D2CB6FA427C5217A00A62B57 /* DDURLSessionDelegate+objc.swift in Sources */, @@ -9097,6 +9150,7 @@ D2EBEE3829BA161100B15732 /* TracingHTTPHeaders.swift in Sources */, D21A94F32B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D2DA2364298D57AA00C6C7E6 /* LaunchTime.swift in Sources */, + 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */, D2DA2365298D57AA00C6C7E6 /* FeatureMessageReceiver.swift in Sources */, D2DA2366298D57AA00C6C7E6 /* Writer.swift in Sources */, D2DA2367298D57AA00C6C7E6 /* Telemetry.swift in Sources */, @@ -9187,6 +9241,7 @@ D2160CE929C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */, D2216EC32A96649500ADAEC8 /* FeatureBaggageTests.swift in Sources */, D2160CDC29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, + 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, D2F44FB8299AA1DA0074B0D9 /* DataCompressionTests.swift in Sources */, D2160CE029C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, D2EBEE3B29BA163E00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, @@ -9233,6 +9288,7 @@ D2EBEE4229BA163F00B15732 /* W3CHTTPHeadersReaderTests.swift in Sources */, D2216EC42A96649700ADAEC8 /* FeatureBaggageTests.swift in Sources */, D2160CDD29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, + 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, D2F44FB9299AA1DB0074B0D9 /* DataCompressionTests.swift in Sources */, D2160CE129C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, D2EBEE3F29BA163F00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index 6dc55f9271..3992068e2e 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -198,22 +198,16 @@ + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughTracerAPI()"> - - - - + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI()"> diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme index 1896fc22ae..d512262352 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme @@ -184,22 +184,22 @@ + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingSampledDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingSampledDistributedTraceWithParent_throughTracerAPI()"> @@ -232,11 +232,6 @@ BlueprintName = "DatadogTraceTests tvOS" ReferencedContainer = "container:Datadog.xcodeproj"> - - - - diff --git a/Datadog/E2ETests/Tracing/TracerE2ETests.swift b/Datadog/E2ETests/Tracing/TracerE2ETests.swift index 1e48e276b7..c90a2176f4 100644 --- a/Datadog/E2ETests/Tracing/TracerE2ETests.swift +++ b/Datadog/E2ETests/Tracing/TracerE2ETests.swift @@ -51,7 +51,7 @@ class TracerE2ETests: E2ETests { /// ``` func test_trace_tracer_inject_span_context() { let anySpan = tracer.startSpan(operationName: .mockRandom()) // this span is never sent - let anyWriter = HTTPHeadersWriter() + let anyWriter = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 20)) measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { tracer.inject(spanContext: anySpan.context, writer: anyWriter) diff --git a/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift b/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift index 6fcd21e108..44cf533a20 100644 --- a/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift +++ b/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift @@ -107,22 +107,19 @@ internal struct DebugManualTraceInjectionView: View { switch selectedTraceHeaderType { case .datadog: - let writer = HTTPHeadersWriter(sampleRate: sampleRate) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: sampleRate)) Tracer.shared().inject(spanContext: span.context, writer: writer) writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } case .w3c: - let writer = W3CHTTPHeadersWriter( - sampleRate: sampleRate, - tracestate: [:] - ) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: sampleRate), tracestate: [:]) Tracer.shared().inject(spanContext: span.context, writer: writer) writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } case .b3Single: - let writer = B3HTTPHeadersWriter(sampleRate: sampleRate, injectEncoding: .single) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: sampleRate), injectEncoding: .single) Tracer.shared().inject(spanContext: span.context, writer: writer) writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } case .b3Multiple: - let writer = B3HTTPHeadersWriter(sampleRate: sampleRate, injectEncoding: .multiple) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: sampleRate), injectEncoding: .multiple) Tracer.shared().inject(spanContext: span.context, writer: writer) writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } } diff --git a/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift index 466d1fc9c8..9507cc345d 100644 --- a/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift @@ -60,7 +60,8 @@ class WebEventIntegrationTests: XCTestCase { "service": "super", "session": { "id": "0110cab4-7471-480e-aa4e-7ce039ced355", - "type": "user" + "type": "user", + "has_replay": true }, "type": "view", "view": { @@ -90,14 +91,16 @@ class WebEventIntegrationTests: XCTestCase { "count": 3 }, "time_spent": 3120000000, - "url": "http://localhost:8080/test.html" + "url": "http://localhost:8080/test.html", }, "_dd": { "document_version": 2, "drift": 0, "format_version": 2, - "session": { - "plan": 2 + "replay_stats": { + "records_count": 10, + "segments_count": 1, + "segments_total_raw_size": 10 } } }, @@ -159,10 +162,7 @@ class WebEventIntegrationTests: XCTestCase { "_dd": { "document_version": 2, "drift": 0, - "format_version": 2, - "session": { - "plan": 1 - } + "format_version": 2 } } """ diff --git a/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift b/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift new file mode 100644 index 0000000000..1738ccd2fe --- /dev/null +++ b/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift @@ -0,0 +1,93 @@ +/* + * 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 +#if !os(tvOS) +import WebKit + +import TestUtilities +@testable import DatadogRUM +@testable import DatadogWebViewTracking +@_spi(Internal) @testable import DatadogSessionReplay + +class WebRecordIntegrationTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var controller: WKUserContentControllerMock! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUp() { + core = DatadogCoreProxy( + context: .mockWith( + env: "test", + version: "1.1.1", + serverTimeOffset: 123 + ) + ) + + controller = WKUserContentControllerMock() + let configuration = WKWebViewConfiguration() + configuration.userContentController = controller + let webView = WKWebView(frame: .zero, configuration: configuration) + WebViewTracking.enable(webView: webView, in: core) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + controller = nil + } + + func testWebRecordIntegration() throws { + // Given + let webView = WKWebView() + let randomApplicationID: String = .mockRandom() + let randomUUID: UUID = .mockRandom() + let randomBrowserViewID: UUID = .mockRandom() + + SessionReplay.enable(with: SessionReplay.Configuration(replaySampleRate: 100), in: core) + RUM.enable(with: .mockWith(applicationID: randomApplicationID) { + $0.uuidGenerator = RUMUUIDGeneratorMock(uuid: randomUUID) + }, in: core) + + let body = """ + { + "eventType": "record", + "event": { + "timestamp" : \(1635932927012), + "type": 2 + }, + "view": { "id": "\(randomBrowserViewID.uuidString.lowercased())" } + } + """ + + // When + RUMMonitor.shared(in: core).startView(key: "web-view") + controller.send(body: body, from: webView) + controller.flush() + + // Then + let segments = try core.waitAndReturnEventsData(ofFeature: SessionReplayFeature.name) + .map { try SegmentJSON($0, source: .ios) } + let segment = try XCTUnwrap(segments.first) + + let expectedUUID = randomUUID.uuidString.lowercased() + let expectedSlotID = String(webView.hash) + + XCTAssertEqual(segment.applicationID, randomApplicationID) + XCTAssertEqual(segment.sessionID, expectedUUID) + XCTAssertEqual(segment.viewID, randomBrowserViewID.uuidString.lowercased()) + + let record = try XCTUnwrap(segment.records.first) + DDAssertDictionariesEqual(record, [ + "timestamp": 1_635_932_927_012 + 123.toInt64Milliseconds, + "type": 2, + "slotId": expectedSlotID + ]) + } +} + +#endif diff --git a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift index 1e28d4cc7a..54a2f32b39 100644 --- a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift +++ b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift @@ -30,9 +30,15 @@ class HeadBasedSamplingTests: XCTestCase { // MARK: - Local Tracing - // TODO: RUM-3470 Enable this test when head-based sampling is supported func testSamplingLocalTrace() throws { - let localTraceSampling: Float = 50 + /* + This is the basic situation of local trace with 3 spans: + + client-ios-app: [-------- parent -----------] | + client-ios-app: [----- child --------] | all 3: keep or drop + client-ios-app: [-- grandchild --] | + */ + let localTraceSampling: Float = 50 // keep or drop // Given traceConfig.sampleRate = localTraceSampling @@ -54,9 +60,14 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped") } - // TODO: RUM-3470 Enable this test when head-based sampling is supported func testSamplingLocalTraceWithImplicitParent() throws { - let localTraceSampling: Float = 50 + /* + This is the situation of local trace with active span as a parent: + + client-ios-app: [-------- active.span -----] | + client-ios-app: [- child1 -][- child2 -] | all 3: keep or drop + */ + let localTraceSampling: Float = 50 // keep or drop // Given traceConfig.sampleRate = localTraceSampling @@ -65,8 +76,8 @@ class HeadBasedSamplingTests: XCTestCase { // When let parent = Tracer.shared(in: core).startSpan(operationName: "parent").setActive() let child1 = Tracer.shared(in: core).startSpan(operationName: "child 1") - let child2 = Tracer.shared(in: core).startSpan(operationName: "child 2") child1.finish() + let child2 = Tracer.shared(in: core).startSpan(operationName: "child 2") child2.finish() parent.finish() @@ -78,10 +89,9 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped") } - // MARK: - Distributed Tracing + // MARK: - Distributed Tracing (through network instrumentation API) - // TODO: RUM-3470 Enable this test when head-based sampling is supported - func testSendingSampledDistributedTraceWithNoParent() throws { + func testSendingSampledDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with the span created with DatadogTrace network instrumentation (with no parent): @@ -111,15 +121,17 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertTrue(span.isKept, "Span must be sampled") // Then - let expectedTraceIDField = String(span.traceID, representation: .decimal) + let expectedTraceIDField = String(span.traceID.idLo) let expectedSpanIDField = String(span.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") } // TODO: RUM-3535 Enable this test when trace context injection control is implemented - func testSendingDroppedDistributedTraceWithNoParent() throws { + func testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with the span created with DatadogTrace network instrumentation (with no parent): @@ -149,15 +161,16 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertFalse(span.isKept, "Span must be dropped") // Then - let expectedTraceIDField = String(span.traceID, representation: .decimal) - let expectedSpanIDField = String(span.spanID, representation: .decimal) + let expectedTraceIDField = span.traceID.idLoHex + let expectedSpanIDField = String(span.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") } - // TODO: RUM-3470 Enable this test when head-based sampling is supported - func testSendingSampledDistributedTraceWithParent() throws { + func testSendingSampledDistributedTraceWithParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with an active local span and is continued with the span created with DatadogTrace network instrumentation: @@ -196,15 +209,17 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID) // Then - let expectedTraceIDField = String(activeSpan.traceID, representation: .decimal) + let expectedTraceIDField = String(activeSpan.traceID.idLo) let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") } // TODO: RUM-3535 Enable this test when trace context injection control is implemented - func testSendingDroppedDistributedTraceWithParent() throws { + func testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with an active local span and is continued with the span created with DatadogTrace network instrumentation: @@ -243,13 +258,191 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID) // Then - let expectedTraceIDField = String(activeSpan.traceID, representation: .decimal) - let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .decimal) + let expectedTraceIDField = activeSpan.traceID.idLoHex + let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + // MARK: - Distributed Tracing (through Tracer API) + + func testSendingSampledDistributedTraceWithNoParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with the span created with Datadog tracer: + + client-ios-app: [------ network.span ------] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: Float = 100 // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + + // Then + let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(networkSpan.operationName, "network.span") + XCTAssertEqual(networkSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(networkSpan.isKept, "Span must be sampled") + + // Then + let expectedTraceIDField = String(networkSpan.traceID.idLo) + let expectedSpanIDField = String(networkSpan.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") } + // TODO: RUM-3535 Enable this test when trace context injection control is implemented + func testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with the span created with Datadog tracer: + + client-ios-app: [------ network.span ------] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: Float = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + + // Then + let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(networkSpan.operationName, "network.span") + XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(networkSpan.isKept, "Span must be dropped") + + // Then + let expectedTraceIDField = networkSpan.traceID.idLoHex + let expectedSpanIDField = String(networkSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + func testSendingSampledDistributedTraceWithParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with Datadog tracer: + + client-ios-app: [-------- active.span -----------] keep + client-ios-app: [------ network.span ------] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: Float = 100 // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + parentSpan.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" })) + + XCTAssertEqual(activeSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(activeSpan.isKept, "Span must be sampled") + XCTAssertEqual(networkSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(networkSpan.isKept, "Span must be sampled") + XCTAssertEqual(networkSpan.traceID, activeSpan.traceID) + XCTAssertEqual(networkSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = String(activeSpan.traceID.idLo) + let expectedSpanIDField = String(networkSpan.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + } + + // TODO: RUM-3535 Enable this test when trace context injection control is implemented + func testSendingDroppedDistributedTraceWithParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with Datadog tracer: + + client-ios-app: [-------- active.span -----------] drop + client-ios-app: [------ network.span ------] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: Float = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + parentSpan.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" })) + + XCTAssertEqual(activeSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(activeSpan.isKept, "Span must be dropped") + XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(networkSpan.isKept, "Span must be dropped") + XCTAssertEqual(networkSpan.traceID, activeSpan.traceID) + XCTAssertEqual(networkSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = activeSpan.traceID.idLoHex + let expectedSpanIDField = String(networkSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + // MARK: - Helpers /// Sends request to `url` using real `URLSession` instrumented with provided `delegate`. diff --git a/DatadogCore/Private/ObjcAppLaunchHandler.m b/DatadogCore/Private/ObjcAppLaunchHandler.m index 2e66e94146..1b2a2dd2dd 100644 --- a/DatadogCore/Private/ObjcAppLaunchHandler.m +++ b/DatadogCore/Private/ObjcAppLaunchHandler.m @@ -4,12 +4,15 @@ * Copyright 2019-Present Datadog, Inc. */ -#import #import #import #import "ObjcAppLaunchHandler.h" +#if TARGET_OS_IOS || TARGET_OS_TV +#import +#endif + // A very long application launch time is most-likely the result of a pre-warmed process. // We consider 30s as a threshold for pre-warm detection. #define COLD_START_TIME_THRESHOLD 30 @@ -39,7 +42,7 @@ + (void)load { // This is called at the `DatadogPrivate` load time, keep the work minimal _shared = [[self alloc] initWithProcessInfo:NSProcessInfo.processInfo loadTime:CFAbsoluteTimeGetCurrent()]; - +#if TARGET_OS_IOS || TARGET_OS_TV NSNotificationCenter * __weak center = NSNotificationCenter.defaultCenter; id __block __unused token = [center addObserverForName:UIApplicationDidBecomeActiveNotification object:nil @@ -55,6 +58,7 @@ + (void)load { [center removeObserver:token]; token = nil; }]; +#endif } + (__dd_private_AppLaunchHandler *)shared { diff --git a/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift b/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift index 58c8bd5a93..26b860049c 100644 --- a/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift +++ b/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if !os(macOS) import Foundation import DatadogInternal @@ -42,3 +43,4 @@ internal struct LaunchTimePublisher: ContextValuePublisher { AppLaunchHandler.shared.setApplicationDidBecomeActiveCallback { _ in } } } +#endif diff --git a/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift b/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift index b24fd3df1e..65b0bb64f1 100644 --- a/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift +++ b/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift @@ -152,9 +152,13 @@ extension NetworkConnectionInfo.Reachability { extension NetworkConnectionInfo.Interface { @available(iOS 2.0, macCatalyst 13.0, *) init?(_ flags: SCNetworkReachabilityFlags?) { + #if os(iOS) || os(tvOS) guard let flags = flags, flags.contains(.isWWAN) else { return nil } self = .cellular + #else + self = .other + #endif } } diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 27f801e3af..7e4e98dfe3 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -417,7 +417,10 @@ extension DatadogContextProvider { self.init(context: context) subscribe(\.serverTimeOffset, to: ServerOffsetPublisher(provider: serverDateProvider)) + + #if !os(macOS) subscribe(\.launchTime, to: LaunchTimePublisher()) + #endif if #available(iOS 12, tvOS 12, *) { subscribe(\.networkConnectionInfo, to: NWPathMonitorPublisher()) diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index 809f848d67..b2d6aba5bc 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -66,8 +66,7 @@ internal class DataUploadWorker: DataUploadWorkerType { self.maxBatchesPerUpload = maxBatchesPerUpload self.featureName = featureName self.telemetry = telemetry - - self.readWork = DispatchWorkItem { [weak self] in + let readWorkItem = DispatchWorkItem { [weak self] in guard let self = self else { return } @@ -87,7 +86,10 @@ internal class DataUploadWorker: DataUploadWorkerType { self.scheduleNextCycle() } } - scheduleNextCycle() + self.readWork = readWorkItem + + // Start sending batches immediately after initialization: + queue.async(execute: readWorkItem) } private func scheduleNextCycle() { diff --git a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift index 1234ab9c2c..6e86010343 100644 --- a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift +++ b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift @@ -38,7 +38,7 @@ internal struct FeatureUpload { ? UIKitBackgroundTaskCoordinator() : nil #else - let backgroundTaskCoordinator = nil + let backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil #endif self.init( diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 70740e327d..4cb14c1dd5 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -372,8 +372,12 @@ public enum Datadog { consolePrint("⚠️ Catalyst is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) #endif + #if os(macOS) + consolePrint("⚠️ macOS is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) + #endif + #if swift(>=5.9) && os(visionOS) - consolePrint("⚠️ VisionOS is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) + consolePrint("⚠️ visionOS is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) #endif do { diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index 4937932e32..2d10253231 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -608,6 +608,11 @@ class DataUploadWorkerTests: XCTestCase { expectTaskEnded.fulfill() } ) + + // Given + writer.write(value: ["k1": "v1"]) + + // When let worker = DataUploadWorker( queue: uploaderQueue, fileReader: reader, @@ -620,7 +625,6 @@ class DataUploadWorkerTests: XCTestCase { maxBatchesPerUpload: .mockRandom(min: 1, max: 100), backgroundTaskCoordinator: backgroundTaskCoordinator ) - writer.write(value: ["k1": "v1"]) // Then withExtendedLifetime(worker) { @@ -638,6 +642,11 @@ class DataUploadWorkerTests: XCTestCase { expectTaskEnded.fulfill() } ) + + // Given + writer.write(value: ["k1": "v1"]) + + // When let worker = DataUploadWorker( queue: uploaderQueue, fileReader: reader, @@ -650,7 +659,6 @@ class DataUploadWorkerTests: XCTestCase { maxBatchesPerUpload: .mockRandom(min: 1, max: 100), backgroundTaskCoordinator: backgroundTaskCoordinator ) - writer.write(value: ["k1": "v1"]) // Then withExtendedLifetime(worker) { diff --git a/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift index d3bdc275dc..0382373eff 100644 --- a/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift @@ -22,9 +22,9 @@ final class WKUserContentControllerMock: WKUserContentController { handlers[name] = nil } - func send(body: Any) { + func send(body: Any, from webView: WKWebView? = nil) { let handler = handlers[DDScriptMessageHandler.name] - let message = WKScriptMessageMock(body: body, name: DDScriptMessageHandler.name) + let message = WKScriptMessageMock(body: body, name: DDScriptMessageHandler.name, webView: webView) handler?.userContentController(self, didReceive: message) } @@ -41,14 +41,17 @@ final class WKUserContentControllerMock: WKUserContentController { private final class WKScriptMessageMock: WKScriptMessage { private let _body: Any private let _name: String + private weak var _webView: WKWebView? - init(body: Any, name: String) { + init(body: Any, name: String, webView: WKWebView? = nil) { _body = body _name = name + _webView = webView } override var body: Any { _body } override var name: String { _name } + override weak var webView: WKWebView? { _webView } } #endif diff --git a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift index fb7fa8454a..496204d1e4 100644 --- a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift @@ -86,13 +86,17 @@ extension DDSpanContext { traceID: TraceID = .mockAny(), spanID: SpanID = .mockAny(), parentSpanID: SpanID? = .mockAny(), - baggageItems: BaggageItems = .mockAny() + baggageItems: BaggageItems = .mockAny(), + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() ) -> DDSpanContext { return DDSpanContext( traceID: traceID, spanID: spanID, parentSpanID: parentSpanID, - baggageItems: baggageItems + baggageItems: baggageItems, + sampleRate: sampleRate, + isKept: isKept ) } } diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index 478cb7b649..c5a1841143 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -725,323 +725,88 @@ class TracerTests: XCTestCase { // MARK: - Injecting span context into carrier - func testItInjectsSpanContextWithHTTPHeadersWriter() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 3, spanID: 4, parentSpanID: .mockAny(), baggageItems: .mockAny()) - - let httpHeadersWriter = HTTPHeadersWriter(sampler: .mockKeepAll()) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) - - // When - tracer.inject(spanContext: spanContext1, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders1 = [ - "x-datadog-trace-id": "64", - "x-datadog-parent-id": "c8", - "x-datadog-sampling-priority": "1", - "x-datadog-tags": "_dd.p.tid=a" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders1) - - // When - tracer.inject(spanContext: spanContext2, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders2 = [ - "x-datadog-trace-id": "3", - "x-datadog-parent-id": "4", - "x-datadog-sampling-priority": "1", - "x-datadog-tags": "_dd.p.tid=0" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders2) - } - - func testItInjectsSpanContextWithB3HTTPHeadersWriter_usingMultipleHeaders() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny()) - let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny()) - - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .multiple) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) - - // When - tracer.inject(spanContext: spanContext1, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders1 = [ - "X-B3-TraceId": "000000000000000a0000000000000064", - "X-B3-SpanId": "00000000000000c8", - "X-B3-Sampled": "1", - "X-B3-ParentSpanId": "0000000000000003" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders1) - - // When - tracer.inject(spanContext: spanContext2, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders2 = [ - "X-B3-TraceId": "00000000000000000000000000000004", - "X-B3-SpanId": "0000000000000005", - "X-B3-Sampled": "1", - "X-B3-ParentSpanId": "0000000000000006" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders2) - - // When - tracer.inject(spanContext: spanContext3, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders3 = [ - "X-B3-TraceId": "0000000000000000000000000000004d", - "X-B3-SpanId": "0000000000000058", - "X-B3-Sampled": "1" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders3) - } - - func testItInjectsSpanContextWithB3HTTPHeadersWriter_usingSingleHeader() { + func testInjectingAndExtractingSpanContextUsingDatadogCarrier() { + // Given Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny()) - let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny()) - - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .single) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - tracer.inject(spanContext: spanContext1, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders1 = [ - "b3": "000000000000000a0000000000000064-00000000000000c8-1-0000000000000003" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders1) - - // When - tracer.inject(spanContext: spanContext2, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders2 = [ - "b3": "00000000000000000000000000000004-0000000000000005-1-0000000000000006" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders2) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + tracer.inject(spanContext: injectedContext, writer: writer) - // When - tracer.inject(spanContext: spanContext3, writer: httpHeadersWriter) + let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! // Then - let expectedHTTPHeaders3 = [ - "b3": "0000000000000000000000000000004d-0000000000000058-1" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders3) + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) } - func testItInjectsRejectedSpanContextWithB3HTTPHeadersWriter_usingSingleHeader() { + func testInjectingAndExtractingSpanContextUsingB3SingleCarrier() { + // Given Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) - - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockRejectAll()) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - tracer.inject(spanContext: spanContext, writer: httpHeadersWriter) + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .single) + tracer.inject(spanContext: injectedContext, writer: writer) - // Then - let expectedHTTPHeaders = [ - "b3": "0" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders) - } - - func testItInjectsRejectedSpanContextWithB3HTTPHeadersWriter_usingMultipleHeader() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) - - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockRejectAll(), injectEncoding: .multiple) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) - - // When - tracer.inject(spanContext: spanContext, writer: httpHeadersWriter) + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! // Then - let expectedHTTPHeaders = [ - "X-B3-Sampled": "0" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders) + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) } - func testItInjectsSpanContextWithW3CHTTPHeadersWriter() { + func testInjectingAndExtractingSpanContextUsingB3MultipleCarrier() { + // Given Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny()) - let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny()) - - let httpHeadersWriter = W3CHTTPHeadersWriter( - sampler: .mockKeepAll(), - tracestate: [ - W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] - ) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - tracer.inject(spanContext: spanContext1, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders1 = [ - "tracestate": "dd=o:rum;p:00000000000000c8;s:1", - "traceparent": "00-000000000000000a0000000000000064-00000000000000c8-01" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders1) + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .multiple) + tracer.inject(spanContext: injectedContext, writer: writer) - // When - tracer.inject(spanContext: spanContext2, writer: httpHeadersWriter) + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! // Then - let expectedHTTPHeaders2 = [ - "tracestate": "dd=o:rum;p:0000000000000005;s:1", - "traceparent": "00-00000000000000000000000000000004-0000000000000005-01" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders2) - - // When - tracer.inject(spanContext: spanContext3, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders3 = [ - "tracestate": "dd=o:rum;p:0000000000000058;s:1", - "traceparent": "00-0000000000000000000000000000004d-0000000000000058-01" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders3) + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) } - func testItInjectsRejectedSpanContextWithW3CHTTPHeadersWriter() { + func testInjectingAndExtractingSpanContextUsingW3CCarrier() { + // Given Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) - - let httpHeadersWriter = W3CHTTPHeadersWriter( - sampler: .mockRejectAll(), - tracestate: [ - W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] - ) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - tracer.inject(spanContext: spanContext, writer: httpHeadersWriter) - - // Then - let expectedHTTPHeaders = [ - "tracestate": "dd=o:rum;p:00000000000000c8;s:0", - "traceparent": "00-000000000000000a0000000000000064-00000000000000c8-00" - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders) - } - - func testItInjectsRejectedSpanContextWithHTTPHeadersWriter() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .headBased) + tracer.inject(spanContext: injectedContext, writer: writer) - let httpHeadersWriter = HTTPHeadersWriter(sampler: .mockRejectAll()) - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) - - // When - tracer.inject(spanContext: spanContext, writer: httpHeadersWriter) + let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! // Then - let expectedHTTPHeaders = [ - "x-datadog-sampling-priority": "0", - ] - XCTAssertEqual(httpHeadersWriter.traceHeaderFields, expectedHTTPHeaders) - } - - func testItExtractsSpanContextWithHTTPHeadersReader() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) - - let httpHeadersWriter = HTTPHeadersWriter(sampler: .mockKeepAll()) - tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) - - let httpHeadersReader = HTTPHeadersReader( - httpHeaderFields: httpHeadersWriter.traceHeaderFields - ) - let extractedSpanContext = tracer.extract(reader: httpHeadersReader) - - XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) - XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) - XCTAssertNil(extractedSpanContext?.dd.parentSpanID) - } - - func testItExtractsSpanContextWithB3HTTPHeadersReader_forMultipleHeaders() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .multiple) - tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) - - let httpHeadersReader = B3HTTPHeadersReader( - httpHeaderFields: httpHeadersWriter.traceHeaderFields - ) - let extractedSpanContext = tracer.extract(reader: httpHeadersReader) - - XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) - XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) - XCTAssertEqual(extractedSpanContext?.dd.parentSpanID, injectedSpanContext.dd.parentSpanID) - } - - func testItExtractsSpanContextWithB3HTTPHeadersReader_forSingleHeader() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .single) - tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) - - let httpHeadersReader = B3HTTPHeadersReader( - httpHeaderFields: httpHeadersWriter.traceHeaderFields - ) - let extractedSpanContext = tracer.extract(reader: httpHeadersReader) - - XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) - XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) - XCTAssertEqual(extractedSpanContext?.dd.parentSpanID, injectedSpanContext.dd.parentSpanID) - } - - func testItExtractsSpanContextWithW3CHTTPHeadersReader() { - Trace.enable(with: config, in: core) - let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - - let httpHeadersWriter = W3CHTTPHeadersWriter( - sampler: .mockKeepAll(), - tracestate: [ - W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] - ) - tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) - - let httpHeadersReader = W3CHTTPHeadersReader( - httpHeaderFields: httpHeadersWriter.traceHeaderFields - ) - let extractedSpanContext = tracer.extract(reader: httpHeadersReader) - - XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) - XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) - XCTAssertNil(extractedSpanContext?.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) } // MARK: - Span Dates Correction diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift index ccf7c76c62..c16950c36d 100644 --- a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -209,7 +209,7 @@ class TracingURLSessionHandlerTests: XCTestCase { func testGivenAllTracingHeaderTypes_itUsesTheSameIds() throws { let request: URLRequest = .mockWith(httpMethod: "GET") - let modifiedRequest = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) + let (modifiedRequest, _) = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) XCTAssertEqual( modifiedRequest.allHTTPHeaderFields, @@ -219,10 +219,10 @@ class TracingURLSessionHandlerTests: XCTestCase { "X-B3-Sampled": "1", "X-B3-TraceId": "000000000000000a0000000000000064", "b3": "000000000000000a0000000000000064-0000000000000064-1", - "x-datadog-trace-id": "64", + "x-datadog-trace-id": "100", "x-datadog-tags": "_dd.p.tid=a", "tracestate": "dd=p:0000000000000064;s:1", - "x-datadog-parent-id": "64", + "x-datadog-parent-id": "100", "x-datadog-sampling-priority": "1" ] ) diff --git a/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift index 0696ba25de..5f274371ca 100644 --- a/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift @@ -69,11 +69,14 @@ class DDConfigurationTests: XCTestCase { XCTAssertEqual(objcConfig.sdkConfiguration.batchProcessingLevel, .high) objcConfig.proxyConfiguration = [kCFNetworkProxiesHTTPEnable: true, kCFNetworkProxiesHTTPPort: 123, kCFNetworkProxiesHTTPProxy: "www.example.com", kCFProxyUsernameKey: "proxyuser", kCFProxyPasswordKey: "proxypass" ] + objcConfig.additionalConfiguration = ["additional": "config"] + XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFNetworkProxiesHTTPEnable] as? Bool, true) XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFNetworkProxiesHTTPPort] as? Int, 123) XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFNetworkProxiesHTTPProxy] as? String, "www.example.com") XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFProxyUsernameKey] as? String, "proxyuser") XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFProxyPasswordKey] as? String, "proxypass") + XCTAssertEqual(objcConfig.sdkConfiguration._internal.additionalConfiguration["additional"] as? String, "config") class ObjCDataEncryption: DDDataEncryption { func encrypt(data: Data) throws -> Data { data } diff --git a/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift index 299bcc7130..40473a017a 100644 --- a/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift @@ -24,7 +24,7 @@ class DDDatadogTests: XCTestCase { super.tearDown() } - // MARK: - Initializing with configuration + // MARK: - SDK initialization / stop lifecycle func testItForwardsInitializationToSwift() throws { let config = DDConfiguration( @@ -50,6 +50,49 @@ class DDDatadogTests: XCTestCase { XCTAssertNil(CoreRegistry.default.get(feature: LogsFeature.self)) } + func testItReflectsInitializationStatus() throws { + let config = DDConfiguration( + clientToken: "abcefghi", + env: "tests" + ) + + config.bundle = .mockWith(CFBundleExecutable: "app-name") + XCTAssertFalse(DDDatadog.isInitialized()) + + DDDatadog.initialize( + configuration: config, + trackingConsent: randomConsent().objc + ) + + XCTAssertTrue(DDDatadog.isInitialized()) + + Datadog.flushAndDeinitialize() + + XCTAssertNil(CoreRegistry.default.get(feature: LogsFeature.self)) + } + + func testItForwardsStopInstanceToSwift() throws { + let config = DDConfiguration( + clientToken: "abcefghi", + env: "tests" + ) + + config.bundle = .mockWith(CFBundleExecutable: "app-name") + + DDDatadog.initialize( + configuration: config, + trackingConsent: randomConsent().objc + ) + + XCTAssertTrue(Datadog.isInitialized()) + + DDDatadog.stopInstance() + + XCTAssertFalse(Datadog.isInitialized()) + + XCTAssertNil(CoreRegistry.default.get(feature: LogsFeature.self)) + } + // MARK: - Changing Tracking Consent func testItForwardsTrackingConsentToSwift() { @@ -92,6 +135,7 @@ class DDDatadogTests: XCTestCase { "attribute-string": "string value" ] ) + DDDatadog.addUserExtraInfo(["foo": "bar"]) XCTAssertEqual(userInfo.current.id, "id") XCTAssertEqual(userInfo.current.name, "name") XCTAssertEqual(userInfo.current.email, "email") @@ -99,6 +143,7 @@ class DDDatadogTests: XCTestCase { XCTAssertEqual(extraInfo["attribute-int"]?.value as? Int, 42) XCTAssertEqual(extraInfo["attribute-double"]?.value as? Double, 42.5) XCTAssertEqual(extraInfo["attribute-string"]?.value as? String, "string value") + XCTAssertEqual(extraInfo["foo"]?.value as? String, "bar") DDDatadog.setUserInfo(id: nil, name: nil, email: nil, extraInfo: [:]) XCTAssertNil(userInfo.current.id) diff --git a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift index 8ea10f6fc7..22c6a1cb70 100644 --- a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift @@ -203,12 +203,12 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDHTTPHeadersWriter(sampleRate: 100) + let objcWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ - "x-datadog-trace-id": "64", - "x-datadog-parent-id": "c8", + "x-datadog-trace-id": "100", + "x-datadog-parent-id": "200", "x-datadog-sampling-priority": "1", "x-datadog-tags": "_dd.p.tid=a" ] @@ -222,7 +222,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDHTTPHeadersWriter(sampleRate: 0) + let objcWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -236,7 +236,7 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDHTTPHeadersWriter(sampleRate: 100) + let objcValidWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) @@ -256,7 +256,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDB3HTTPHeadersWriter(sampleRate: 100) + let objcWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -272,7 +272,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDB3HTTPHeadersWriter(sampleRate: 0) + let objcWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -286,7 +286,7 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDB3HTTPHeadersWriter(sampleRate: 100) + let objcValidWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) @@ -306,7 +306,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDW3CHTTPHeadersWriter(sampleRate: 100) + let objcWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -323,7 +323,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDW3CHTTPHeadersWriter(sampleRate: 0) + let objcWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -338,7 +338,7 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDW3CHTTPHeadersWriter(sampleRate: 100) + let objcValidWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m index 57beff0c08..e315e3ec02 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m @@ -19,7 +19,10 @@ @implementation DDB3HTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDB3HTTPHeadersWriter alloc] initWithSampleRate:100 injectEncoding:DDInjectEncodingSingle]; + [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] + injectEncoding:DDInjectEncodingSingle]; + [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] + injectEncoding:DDInjectEncodingMultiple]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m index eb49eb8f87..1b5bedcb3d 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m @@ -63,6 +63,7 @@ - (void)testDDConfigurationBuilderAPI { configuration.bundle = [NSBundle mainBundle]; configuration.batchSize = DDBatchSizeMedium; configuration.uploadFrequency = DDUploadFrequencyAverage; + configuration.additionalConfiguration = @{@"additional": @"config"}; [configuration setEncryption:[CustomDDDataEncryption new]]; } diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m index c1446779f2..1fd8d2c746 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m @@ -29,14 +29,17 @@ - (void)testDDDatadog { [DDDatadog initializeWithConfiguration:configuration trackingConsent:[DDTrackingConsent notGranted]]; + [DDDatadog isInitialized]; + DDSDKVerbosityLevel verbosity = [DDDatadog verbosityLevel]; [DDDatadog setVerbosityLevel:verbosity]; [DDDatadog setUserInfoWithId:@"" name:@"" email:@"" extraInfo:@{}]; + [DDDatadog addUserExtraInfo:@{}]; [DDDatadog setTrackingConsentWithConsent:[DDTrackingConsent notGranted]]; [DDDatadog clearAllData]; - [DDDatadog flushAndDeinitialize]; + [DDDatadog stopInstance]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m index cab2afaeb6..7c0f0e9ffc 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m @@ -19,7 +19,8 @@ @implementation DDHTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDHTTPHeadersWriter alloc] initWithSampleRate:50]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased]]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50]]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m index f75ea8ce27..1c5486d0ae 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m @@ -19,7 +19,9 @@ @implementation DDW3CHTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDW3CHTTPHeadersWriter alloc] initWithSampleRate:50]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased]]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50]]; + } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift index d832985aaf..b58ecc9804 100644 --- a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift +++ b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift @@ -271,11 +271,6 @@ internal class RUMSessionMatcher { private func validate(rumViewEvents: [RUMViewEvent]) throws { // All view events must use `session.plan` "lite" try rumViewEvents.forEach { viewEvent in - if viewEvent.dd.session?.plan != .plan1 { - throw RUMSessionConsistencyException( - description: "All RUM events must use session plan `1` (RUM Lite). Bad view event: \(viewEvent)" - ) - } if viewEvent.source == .ios { // validete only mobile events try validate(device: viewEvent.device) try validate(os: viewEvent.os) @@ -286,11 +281,6 @@ private func validate(rumViewEvents: [RUMViewEvent]) throws { private func validate(rumActionEvents: [RUMActionEvent]) throws { // All action events must use `session.plan` "lite" try rumActionEvents.forEach { actionEvent in - if actionEvent.dd.session?.plan != .plan1 { - throw RUMSessionConsistencyException( - description: "All RUM events must use session plan `1` (RUM Lite). Bad action event: \(actionEvent)" - ) - } if actionEvent.source == .ios { // validete only mobile events try validate(device: actionEvent.device) try validate(os: actionEvent.os) @@ -309,11 +299,6 @@ private func validate(rumResourceEvents: [RUMResourceEvent]) throws { // All resource events must use `session.plan` "lite" try rumResourceEvents.forEach { resourceEvent in - if resourceEvent.dd.session?.plan != .plan1 { - throw RUMSessionConsistencyException( - description: "All RUM events must use session plan `1` (RUM Lite). Bad resource event: \(resourceEvent)" - ) - } if resourceEvent.source == .ios { // validete only mobile events try validate(device: resourceEvent.device) try validate(os: resourceEvent.os) @@ -324,11 +309,6 @@ private func validate(rumResourceEvents: [RUMResourceEvent]) throws { private func validate(rumErrorEvents: [RUMErrorEvent]) throws { // All error events must use `session.plan` "lite" try rumErrorEvents.forEach { errorEvent in - if errorEvent.dd.session?.plan != .plan1 { - throw RUMSessionConsistencyException( - description: "All RUM events must use session plan `1` (RUM Lite). Bad error event: \(errorEvent)" - ) - } if errorEvent.source == .ios { // validete only mobile events try validate(device: errorEvent.device) try validate(os: errorEvent.os) @@ -339,11 +319,6 @@ private func validate(rumErrorEvents: [RUMErrorEvent]) throws { private func validate(rumLongTaskEvents: [RUMLongTaskEvent]) throws { // All error events must use `session.plan` "lite" try rumLongTaskEvents.forEach { longTaskEvent in - if longTaskEvent.dd.session?.plan != .plan1 { - throw RUMSessionConsistencyException( - description: "All RUM events must use session plan `1` (RUM Lite). Bad long task event: \(longTaskEvent)" - ) - } if longTaskEvent.source == .ios { // validete only mobile events try validate(device: longTaskEvent.device) try validate(os: longTaskEvent.os) diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift index dc3c62ae45..99b2f885b5 100644 --- a/DatadogInternal/Sources/Context/DeviceInfo.swift +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -49,10 +49,10 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { } } -#if canImport(UIKit) +import MachO +#if canImport(UIKit) import UIKit -import MachO extension DeviceInfo { /// Creates device info based on UIKit description. @@ -96,4 +96,33 @@ extension DeviceInfo { #endif } } +#elseif os(macOS) +/// Creates device info based on Host description. +/// +/// - Parameters: +/// - processInfo: The current process information. +extension DeviceInfo { + public init( + processInfo: ProcessInfo = .processInfo + ) { + var architecture = "unknown" + if let archInfo = NXGetLocalArchInfo()?.pointee { + architecture = String(utf8String: archInfo.name) ?? "unknown" + } + Host.current().name + + let build = (try? Sysctl.osVersion()) ?? "" + let model = (try? Sysctl.model()) ?? "" + let systemVersion = processInfo.operatingSystemVersion + + self.init( + name: model.components(separatedBy: CharacterSet.letters.inverted).joined(), + model: model, + osName: "macOS", + osVersion: "\(systemVersion.majorVersion).\(systemVersion.minorVersion).\(systemVersion.patchVersion)", + osBuildNumber: build, + architecture: architecture + ) + } +} #endif diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift index e8d5acd30f..58a9fea544 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift @@ -45,4 +45,14 @@ public class B3HTTPHeadersReader: TracePropagationHeadersReader { return nil } + + public var sampled: Bool? { + if let single = httpHeaderFields[B3HTTPHeaders.Single.b3Field] { + return single != B3HTTPHeaders.Constants.unsampledValue + } else if let multiple = httpHeaderFields[B3HTTPHeaders.Multiple.sampledField] { + return multiple == B3HTTPHeaders.Constants.sampledValue + } + + return nil + } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift index b6b219d9e6..7f34da3e7a 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift @@ -57,11 +57,7 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// public private(set) var traceHeaderFields: [String: String] = [:] - /// The tracing sampler. - /// - /// The sample rate determines the `X-B3-Sampled` header field value - /// and whether `X-B3-TraceId`, `X-B3-SpanId`, and `X-B3-ParentSpanId` are propagated. - private let sampler: Sampler + private let samplingStrategy: TraceSamplingStrategy /// The telemetry header encoding used by the writer. private let injectEncoding: InjectEncoding @@ -70,7 +66,7 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// /// - Parameter samplingRate: The sampling rate applied for headers injection. /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:injectEncoding:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init( samplingRate: Float, injectEncoding: InjectEncoding = .single @@ -82,25 +78,26 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init( sampleRate: Float = 20, injectEncoding: InjectEncoding = .single ) { self.init( - sampler: Sampler(samplingRate: sampleRate), + samplingStrategy: .custom(sampleRate: sampleRate), injectEncoding: injectEncoding ) } /// Initializes the headers writer. /// - /// - Parameter sampler: The sampler used for headers injection. + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. public init( - sampler: Sampler, + samplingStrategy: TraceSamplingStrategy, injectEncoding: InjectEncoding = .single ) { - self.sampler = sampler + self.samplingStrategy = samplingStrategy self.injectEncoding = injectEncoding } @@ -109,29 +106,30 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter traceID: The trace ID. /// - Parameter spanID: The span ID. /// - Parameter parentSpanID: The parent span ID, if applicable. - public func write(traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) { - let samplingPriority = sampler.sample() + public func write(traceContext: TraceContext) { + let sampler = samplingStrategy.sampler(for: traceContext) + let sampled = sampler.sample() typealias Constants = B3HTTPHeaders.Constants switch injectEncoding { case .multiple: traceHeaderFields = [ - B3HTTPHeaders.Multiple.sampledField: samplingPriority ? Constants.sampledValue : Constants.unsampledValue + B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue ] - if samplingPriority { - traceHeaderFields[B3HTTPHeaders.Multiple.traceIDField] = String(traceID, representation: .hexadecimal32Chars) - traceHeaderFields[B3HTTPHeaders.Multiple.spanIDField] = String(spanID, representation: .hexadecimal16Chars) - traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = parentSpanID.map { String($0, representation: .hexadecimal16Chars) } + if sampled { + traceHeaderFields[B3HTTPHeaders.Multiple.traceIDField] = String(traceContext.traceID, representation: .hexadecimal32Chars) + traceHeaderFields[B3HTTPHeaders.Multiple.spanIDField] = String(traceContext.spanID, representation: .hexadecimal16Chars) + traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } } case .single: - if samplingPriority { + if sampled { traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [ - String(traceID, representation: .hexadecimal32Chars), - String(spanID, representation: .hexadecimal16Chars), - samplingPriority ? Constants.sampledValue : Constants.unsampledValue, - parentSpanID.map { String($0, representation: .hexadecimal16Chars) } + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), + sampled ? Constants.sampledValue : Constants.unsampledValue, + traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } ] .compactMap { $0 } .joined(separator: Constants.b3Separator) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift index 300eaea577..4a5b578265 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift @@ -16,7 +16,7 @@ public class HTTPHeadersReader: TracePropagationHeadersReader { public func read() -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { guard let traceIDLoValue = httpHeaderFields[TracingHTTPHeaders.traceIDField], let spanIDValue = httpHeaderFields[TracingHTTPHeaders.parentSpanIDField], - let spanID = SpanID(spanIDValue, representation: .hexadecimal) + let spanID = SpanID(spanIDValue, representation: .decimal) else { return nil } @@ -34,7 +34,7 @@ public class HTTPHeadersReader: TracePropagationHeadersReader { let traceID = TraceID( idHi: UInt64(traceIDHiValue, radix: 16) ?? 0, - idLo: UInt64(traceIDLoValue, radix: 16) ?? 0 + idLo: UInt64(traceIDLoValue, radix: 10) ?? 0 ) return ( @@ -43,4 +43,11 @@ public class HTTPHeadersReader: TracePropagationHeadersReader { parentSpanID: nil ) } + + public var sampled: Bool? { + if let sampling = httpHeaderFields[TracingHTTPHeaders.samplingPriorityField] { + return sampling == "1" + } + return nil + } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift index 6f6e26187c..b634f27d54 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift @@ -34,16 +34,12 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// public private(set) var traceHeaderFields: [String: String] = [:] - /// The tracing sampler. - /// - /// This value will decide of the `x-datadog-sampling-priority` header field value - /// and if `x-datadog-trace-id` and `x-datadog-parent-id` are propagated. - private let sampler: Sampler + private let samplingStrategy: TraceSamplingStrategy /// Initializes the headers writer. /// /// - Parameter samplingRate: The sampling rate applied for headers injection. - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate) } @@ -51,15 +47,16 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// Initializes the headers writer. /// /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(sampleRate: Float = 20) { - self.init(sampler: Sampler(samplingRate: sampleRate)) + self.init(samplingStrategy: .custom(sampleRate: sampleRate)) } /// Initializes the headers writer. /// - /// - Parameter sampler: The sampler used for headers injection. - public init(sampler: Sampler) { - self.sampler = sampler + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. + public init(samplingStrategy: TraceSamplingStrategy) { + self.samplingStrategy = samplingStrategy } /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. @@ -67,17 +64,18 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter traceID: The trace ID. /// - Parameter spanID: The span ID. /// - Parameter parentSpanID: The parent span ID, if applicable. - public func write(traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) { - let samplingPriority = sampler.sample() + public func write(traceContext: TraceContext) { + let sampler = samplingStrategy.sampler(for: traceContext) + let sampled = sampler.sample() traceHeaderFields = [ - TracingHTTPHeaders.samplingPriorityField: samplingPriority ? "1" : "0" + TracingHTTPHeaders.samplingPriorityField: sampled ? "1" : "0" ] - if samplingPriority { - traceHeaderFields[TracingHTTPHeaders.traceIDField] = traceID.idLoHex - traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(spanID, representation: .hexadecimal) - traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceID.idHiHex)" + if sampled { + traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo) + traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal) + traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)" } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift index c8845cc597..91aaf0e760 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift @@ -8,35 +8,33 @@ import Foundation /// An interface for processing `URLSession` task interceptions. public protocol DatadogURLSessionHandler { - /// The interceptor's first party hosts + /// The first party hosts configured for this handler. var firstPartyHosts: FirstPartyHosts { get } - /// Tells the interceptor to modify a URL request. + /// Modifies the provided request by injecting trace headers. /// /// - Parameters: - /// - request: The request to intercept. - /// - additionalFirstPartyHosts: Additional 1st-party hosts. - /// - Returns: The modified request. - func modify(request: URLRequest, headerTypes: Set) -> URLRequest + /// - request: The request to be modified. + /// - headerTypes: The types of tracing headers to inject into the request. + /// - Returns: A tuple containing the modified request and the injected TraceContext. If no trace is injected (e.g., due to sampling), + /// the returned request remains unmodified, and the trace context will be nil. + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) - /// Returns the trace of the current execution context. - func traceContext() -> TraceContext? - - /// Tells the interceptor that the session did start. + /// Notifies the handler that the interception has started. /// - /// - Parameter interception: The URLSession interception. + /// - Parameter interception: The URLSession task interception. func interceptionDidStart(interception: URLSessionTaskInterception) - /// Tells the interceptor that the session did complete. + /// Notifies the handler that the interception has completed. /// - /// - Parameter interception: The URLSession interception. + /// - Parameter interception: The URLSession task interception. func interceptionDidComplete(interception: URLSessionTaskInterception) } extension DatadogCoreProtocol { /// Core extension for registering `URLSession` handlers. /// - /// - Parameter urlSessionHandler: The `URLSession` handlers to register. + /// - Parameter urlSessionHandler: The `URLSession` handler to register. public func register(urlSessionHandler: DatadogURLSessionHandler) throws { let feature = get(feature: NetworkInstrumentationFeature.self) ?? .init() feature.handlers.append(urlSessionHandler) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift index 38a5eeae0a..c1f8f81495 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift @@ -78,12 +78,15 @@ internal final class NetworkInstrumentationFeature: DatadogFeature { return } + var injectedTraceContexts: [TraceContext]? + if let currentRequest = task.currentRequest { - let request = self.intercept(request: currentRequest, additionalFirstPartyHosts: configuredFirstPartyHosts) + let (request, traceContexts) = self.intercept(request: currentRequest, additionalFirstPartyHosts: configuredFirstPartyHosts) task.dd.override(currentRequest: request) + injectedTraceContexts = traceContexts } - self.intercept(task: task, additionalFirstPartyHosts: configuredFirstPartyHosts) + self.intercept(task: task, with: injectedTraceContexts ?? [], additionalFirstPartyHosts: configuredFirstPartyHosts) } ) @@ -128,31 +131,43 @@ internal final class NetworkInstrumentationFeature: DatadogFeature { } extension NetworkInstrumentationFeature { - /// Tells the interceptors to modify a URL request. + /// Intercepts the provided request by injecting trace headers based on first-party hosts configuration. + /// + /// Only requests with URLs that match the list of first-party hosts have tracing headers injected. /// /// - Parameters: /// - request: The request to intercept. - /// - additionalFirstPartyHosts: Extra hosts to consider in the interception - /// - Returns: The modified request. - func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts?) -> URLRequest { + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception, used in conjunction with hosts defined in each handler. + /// - Returns: A tuple containing the modified request and the list of injected TraceContexts, one or none for each handler. If no trace is injected (e.g., due to sampling), + /// the list will be empty. + func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts?) -> (URLRequest, [TraceContext]) { let headerTypes = firstPartyHosts(with: additionalFirstPartyHosts) .tracingHeaderTypes(for: request.url) guard !headerTypes.isEmpty else { - return request + return (request, []) } - return handlers.reduce(request) { - $1.modify(request: $0, headerTypes: headerTypes) + var request = request + var traceContexts: [TraceContext] = [] // each handler can inject distinct trace context + for handler in handlers { + let (nextRequest, nextTraceContext) = handler.modify(request: request, headerTypes: headerTypes) + request = nextRequest + if let nextTraceContext = nextTraceContext { + traceContexts.append(nextTraceContext) + } } + + return (request, traceContexts) } - /// Tells the interceptors that a task was created. + /// Intercepts the provided URLSession task by creating an interception object and notifying all handlers that the interception has started. /// /// - Parameters: - /// - task: The created task. - /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. - func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts?) { + /// - task: The URLSession task to intercept. + /// - injectedTraceContexts: The list of trace contexts injected into the task's request, one or none for each handler. + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception, used in conjunction with hosts defined in each handler. + func intercept(task: URLSessionTask, with injectedTraceContexts: [TraceContext], additionalFirstPartyHosts: FirstPartyHosts?) { // In response to https://github.com/DataDog/dd-sdk-ios/issues/1638 capture the current request object on the // caller thread and freeze its attributes through `ImmutableRequest`. This is to avoid changing the request // object from multiple threads: @@ -161,9 +176,6 @@ extension NetworkInstrumentationFeature { } let request = ImmutableRequest(request: currentRequest) - // Get the current trace context from all handlers. - let traceContexts = handlers.compactMap { $0.traceContext() } - queue.async { [weak self] in guard let self = self else { return @@ -179,20 +191,10 @@ extension NetworkInstrumentationFeature { interception.register(request: request) - if let trace = self.extractTrace(firstPartyHosts: firstPartyHosts, request: request) { - // The parent span id is extracted from the headers unless - // the propagation headers does not support it (only B3 does). - // In that case, we register the current trace context as parent - // if the trace ID matches. - let parentSpanID = trace.parentSpanID ?? - traceContexts.first(where: { $0.traceID == trace.traceID })?.spanID - - // Register the trace with parent - interception.register(trace: TraceContext( - traceID: trace.traceID, - spanID: trace.spanID, - parentSpanID: parentSpanID - )) + if let traceContext = injectedTraceContexts.first { + // ^ If multiple trace contexts were injected (one per each handler) take the first one. This mimics the implicit + // behaviour from before RUM-3470. + interception.register(trace: traceContext) } if let origin = request.allHTTPHeaderFields?[TracingHTTPHeaders.originField] { @@ -266,24 +268,6 @@ extension NetworkInstrumentationFeature { handlers.forEach { $0.interceptionDidComplete(interception: interception) } interceptions[task] = nil } - - private func extractTrace(firstPartyHosts: FirstPartyHosts, request: ImmutableRequest) -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { - guard let headers = request.allHTTPHeaderFields else { - return nil - } - - let tracingHeaderTypes = firstPartyHosts.tracingHeaderTypes(for: request.url) - - let reader: TracePropagationHeadersReader - if tracingHeaderTypes.contains(.datadog) { - reader = HTTPHeadersReader(httpHeaderFields: headers) - } else if tracingHeaderTypes.contains(.b3) || tracingHeaderTypes.contains(.b3multi) { - reader = B3HTTPHeadersReader(httpHeaderFields: headers) - } else { - reader = W3CHTTPHeadersReader(httpHeaderFields: headers) - } - return reader.read() - } } extension NetworkInstrumentationFeature: Flushable { diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift new file mode 100644 index 0000000000..c8a9316068 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift @@ -0,0 +1,45 @@ +/* + * 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 + +/// A context used to propagate trace through HTTP request headers. +public struct TraceContext: Equatable { + /// The unique identifier for the trace. + public let traceID: TraceID + /// The unique identifier for the span. + public let spanID: SpanID + /// The unique identifier for the parent span, if any. + public let parentSpanID: SpanID? + /// The sample rate used for injecting the span into a request. + /// + /// It is a value between `0.0` (drop) and `100.0` (keep), determined by the local or distributed trace sampler. + public let sampleRate: Float + /// Indicates whether this span was sampled or rejected by the sampler. + public let isKept: Bool + + /// Initializes a `TraceContext` instance with the provided parameters. + /// + /// - Parameters: + /// - traceID: The unique identifier for the trace. + /// - spanID: The unique identifier for the span. + /// - parentSpanID: The unique identifier for the parent span, if any. + /// - sampleRate: The sample rate used for injecting the span into a request. + /// - isKept: A boolean indicating whether this span was sampled or rejected by the sampler. + public init( + traceID: TraceID, + spanID: SpanID, + parentSpanID: SpanID?, + sampleRate: Float, + isKept: Bool + ) { + self.traceID = traceID + self.spanID = spanID + self.parentSpanID = parentSpanID + self.sampleRate = sampleRate + self.isKept = isKept + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift index 9665a57ba7..9de4c9de12 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift @@ -13,4 +13,7 @@ public protocol TracePropagationHeadersReader { spanID: SpanID, parentSpanID: SpanID? )? + + /// Indicates whether the trace was sampled based on the provided headers. + var sampled: Bool? { get } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift index e1c1bd910f..1429c1a504 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift @@ -6,18 +6,33 @@ import Foundation +/// Available strategies for sampling trace propagation headers. +public enum TraceSamplingStrategy { + /// Trace propagation headers will be sampled same as propagated span. + /// + /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace + /// is determined from the first span of the trace, the head, when the trace is created. With `.headBased` + /// strategy, this decision is propagated through the request context to downstream services. + case headBased + /// Trace propagation headers will be sampled independently from sampling decision in propagated span. + /// + /// Use this option to apply the provided `sampleRate` for determining the decision to keep or drop the trace + /// in downstream services independently of sampling their parent span. + case custom(sampleRate: Float) + + internal func sampler(for traceContext: TraceContext) -> Sampling { + switch self { + case .headBased: + return DeterministicSampler(shouldSample: traceContext.isKept, samplingRate: traceContext.sampleRate) + case .custom(let sampleRate): + return Sampler(samplingRate: sampleRate) + } + } +} + /// Write interface for a custom carrier public protocol TracePropagationHeadersWriter { var traceHeaderFields: [String: String] { get } - /// Inject a span context into the custom carrier - /// - /// - parameter spanContext: context to inject into the custom carrier - func write(traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) -} - -extension TracePropagationHeadersWriter { - public func write(traceID: TraceID, spanID: SpanID) { - write(traceID: traceID, spanID: spanID, parentSpanID: nil) - } + func write(traceContext: TraceContext) } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift index a141f37fe0..d949b3c5a5 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift @@ -127,12 +127,15 @@ open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate { return } + var injectedTraceContexts: [TraceContext]? + if let currentRequest = task.currentRequest { - let request = feature.intercept(request: currentRequest, additionalFirstPartyHosts: firstPartyHosts) + let (request, traceContexts) = feature.intercept(request: currentRequest, additionalFirstPartyHosts: firstPartyHosts) task.dd.override(currentRequest: request) + injectedTraceContexts = traceContexts } - feature.intercept(task: task, additionalFirstPartyHosts: firstPartyHosts) + feature.intercept(task: task, with: injectedTraceContexts ?? [], additionalFirstPartyHosts: firstPartyHosts) } ) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift index 97158aef2a..6892509bfd 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift @@ -6,6 +6,8 @@ import Foundation +// TODO: RUM-3470 Add tests to `URLSessionInterceptor` + /// The `URLSession` Interceptor provides methods for injecting distributed-traces /// headers into a `URLRequest`and to instrument a `URLURLSessionTask` lifcycle, /// from its creation to completion. @@ -27,6 +29,13 @@ public struct URLSessionInterceptor { return URLSessionInterceptor(feature: feature) } + /// Maps the trace ID to the full trace context generated for that trace. + /// + /// This is to bridge the gap between what is encoded into HTTP headers and what is later needed for processing + /// the interception (unlike request headers, the `TraceContext` holds the original information on trace sampling). + @ReadWriteLock + private var contextsByTraceID: [TraceID: [TraceContext]] = [:] + /// Tells the interceptor to modify a URL request. /// /// - Parameters: @@ -34,7 +43,11 @@ public struct URLSessionInterceptor { /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. /// - Returns: The modified request. public func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts? = nil) -> URLRequest { - feature.intercept(request: request, additionalFirstPartyHosts: additionalFirstPartyHosts) + let (request, traceContexts) = feature.intercept(request: request, additionalFirstPartyHosts: additionalFirstPartyHosts) + if let traceID = extractTraceID(from: request) { + contextsByTraceID[traceID] = traceContexts + } + return request } /// Tells the interceptors that a task was created. @@ -43,7 +56,12 @@ public struct URLSessionInterceptor { /// - task: The created task. /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. public func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts? = nil) { - feature.intercept(task: task, additionalFirstPartyHosts: additionalFirstPartyHosts) + var injectedTraceContexts: [TraceContext] = [] + if let request = task.currentRequest, let traceID = extractTraceID(from: request) { + injectedTraceContexts = contextsByTraceID[traceID] ?? [] + } + + feature.intercept(task: task, with: injectedTraceContexts, additionalFirstPartyHosts: additionalFirstPartyHosts) } /// Tells the interceptor that metrics were collected for the given task. @@ -71,5 +89,28 @@ public struct URLSessionInterceptor { /// - error: If an error occurred, an error object indicating how the transfer failed, otherwise NULL. public func task(_ task: URLSessionTask, didCompleteWithError error: Error?) { feature.task(task, didCompleteWithError: error) + + if let request = task.currentRequest, let traceID = extractTraceID(from: request) { + contextsByTraceID[traceID] = nil + } + } + + // MARK: - Private + + private func extractTraceID(from request: URLRequest) -> TraceID? { + guard let headers = request.allHTTPHeaderFields else { + return nil + } + + // Try all supported header types until first one is matched: + if let dd = HTTPHeadersReader(httpHeaderFields: headers).read() { + return dd.traceID + } else if let b3 = B3HTTPHeadersReader(httpHeaderFields: headers).read() { + return b3.traceID + } else if let w3c = W3CHTTPHeadersReader(httpHeaderFields: headers).read() { + return w3c.traceID + } + + return nil } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift index 609bfc1996..22486f7b3b 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift @@ -71,22 +71,6 @@ public class URLSessionTaskInterception { } } -public struct TraceContext { - public let traceID: TraceID - public let spanID: SpanID - public let parentSpanID: SpanID? - - public init( - traceID: TraceID, - spanID: SpanID, - parentSpanID: SpanID? = nil - ) { - self.traceID = traceID - self.spanID = spanID - self.parentSpanID = parentSpanID - } -} - public struct ResourceCompletion { public let httpResponse: HTTPURLResponse? public let error: Error? diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift index 9b2effe9c9..b8034ddaf5 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift @@ -33,4 +33,15 @@ public class W3CHTTPHeadersReader: TracePropagationHeadersReader { parentSpanID: nil ) } + + public var sampled: Bool? { + if let traceparent = httpHeaderFields[W3CHTTPHeaders.traceparent] { + guard let sampled = traceparent.components(separatedBy: W3CHTTPHeaders.Constants.separator).last else { + return nil + } + return sampled == W3CHTTPHeaders.Constants.sampledValue + } + + return nil + } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift index 20f6bb7367..036f07feca 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -38,17 +38,13 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// This value will be merged with the tracestate from the trace context. private let tracestate: [String: String] - /// The tracing sampler. - /// - /// This value will decide of the `FLAG_SAMPLED` header field value - /// and if `trace-id`, `span-id` are propagated. - private let sampler: Sampler + private let samplingStrategy: TraceSamplingStrategy /// Initializes the headers writer. /// /// - Parameter samplingRate: The sampling rate applied for headers injection. /// - Parameter tracestate: The tracestate to be injected. - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate, tracestate: [:]) } @@ -57,16 +53,17 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. /// - Parameter tracestate: The tracestate to be injected. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(sampleRate: Float = 20, tracestate: [String: String] = [:]) { - self.init(sampler: Sampler(samplingRate: sampleRate), tracestate: tracestate) + self.init(samplingStrategy: .custom(sampleRate: sampleRate), tracestate: tracestate) } /// Initializes the headers writer. /// - /// - Parameter sampler: The sampler used for headers injection. + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. /// - Parameter tracestate: The tracestate to be injected. - public init(sampler: Sampler, tracestate: [String: String]) { - self.sampler = sampler + public init(samplingStrategy: TraceSamplingStrategy, tracestate: [String: String] = [:]) { + self.samplingStrategy = samplingStrategy self.tracestate = tracestate } @@ -75,15 +72,16 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter traceID: The trace ID. /// - Parameter spanID: The span ID. /// - Parameter parentSpanID: The parent span ID, if applicable. - public func write(traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) { + public func write(traceContext: TraceContext) { typealias Constants = W3CHTTPHeaders.Constants + let sampler = samplingStrategy.sampler(for: traceContext) let sampled = sampler.sample() traceHeaderFields[W3CHTTPHeaders.traceparent] = [ Constants.version, - String(traceID, representation: .hexadecimal32Chars), - String(spanID, representation: .hexadecimal16Chars), + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), sampled ? Constants.sampledValue : Constants.unsampledValue ] .joined(separator: Constants.separator) @@ -92,7 +90,7 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { // over the ones from the trace context let tracestate: [String: String] = [ Constants.sampling: "\(sampled ? 1 : 0)", - Constants.parentId: String(spanID, representation: .hexadecimal16Chars) + Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) ].merging(tracestate) { old, new in return new } diff --git a/DatadogInternal/Sources/Upload/DefaultJSONEncoder.swift b/DatadogInternal/Sources/Upload/DefaultJSONEncoder.swift index 69a7f96138..e6fc899d85 100644 --- a/DatadogInternal/Sources/Upload/DefaultJSONEncoder.swift +++ b/DatadogInternal/Sources/Upload/DefaultJSONEncoder.swift @@ -16,7 +16,7 @@ extension DatadogExtension where ExtendedType == JSONEncoder { let formatted = iso8601DateFormatter.string(from: date) try container.encode(formatted) } - if #available(iOS 13, tvOS 13, macOS 10.15, *) { + if #available(iOS 13, tvOS 13, *) { encoder.outputFormatting = [.withoutEscapingSlashes] } return encoder diff --git a/DatadogInternal/Sources/Utils/Sampler.swift b/DatadogInternal/Sources/Utils/Sampler.swift index 7a3abb009c..e87a82f981 100644 --- a/DatadogInternal/Sources/Utils/Sampler.swift +++ b/DatadogInternal/Sources/Utils/Sampler.swift @@ -6,8 +6,17 @@ import Foundation +/// Protocol for determining sampling decisions. +public protocol Sampling { + /// Determines whether sampling should be performed. + /// + /// - Returns: A boolean value indicating whether sampling should occur. + /// `true` if the sample should be kept, `false` if it should be dropped. + func sample() -> Bool +} + /// Sampler, deciding if events should be sent do Datadog or dropped. -public struct Sampler { +public struct Sampler: Sampling { /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. public let samplingRate: Float @@ -21,3 +30,18 @@ public struct Sampler { return Float.random(in: 0.0..<100.0) < samplingRate } } + +/// A sampler that determines sampling decisions deterministically (the same each time). +internal struct DeterministicSampler: Sampling { + /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. + let samplingRate: Float + /// Persisted sampling decision. + private let shouldSample: Bool + + init(shouldSample: Bool, samplingRate: Float) { + self.samplingRate = samplingRate + self.shouldSample = shouldSample + } + + func sample() -> Bool { shouldSample } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift index 62575ba8bf..cec2091c1c 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift @@ -5,6 +5,7 @@ */ import XCTest +import TestUtilities import DatadogInternal class B3HTTPHeadersReaderTests: XCTestCase { @@ -80,19 +81,21 @@ class B3HTTPHeadersReaderTests: XCTestCase { func testReadingSampledTraceContext() { let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! - let writer = B3HTTPHeadersWriter(sampleRate: 100, injectEncoding: encoding) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), injectEncoding: encoding) + writer.write(traceContext: .mockRandom()) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) } func testReadingNotSampledTraceContext() { let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! - let writer = B3HTTPHeadersWriter(sampleRate: 0, injectEncoding: encoding) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding) + writer.write(traceContext: .mockRandom()) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift index 9565b26f30..7491012888 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift @@ -5,37 +5,80 @@ */ import XCTest -@testable import DatadogInternal +import TestUtilities +import DatadogInternal class B3HTTPHeadersWriterTests: XCTestCase { - func testItWritesSingleHeader() { - let sampler: Sampler = .mockKeepAll() + func testWritingSampledTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .single ) writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: true + ) ) let headers = writer.traceHeaderFields XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1-000000000000162e") } - func testItWritesSingleHeaderWithSampling() { - let sampler: Sampler = .mockRejectAll() + func testWritingDroppedTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .single ) writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0") + } + + func testWritingSampledTraceContext_withSingleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + injectEncoding: .single + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1-000000000000162e") + } + + func testWritingDroppedTraceContext_withSingleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + injectEncoding: .single + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) ) let headers = writer.traceHeaderFields @@ -43,30 +86,80 @@ class B3HTTPHeadersWriterTests: XCTestCase { } func testItWritesSingleHeaderWithoutOptionalValues() { - let sampler: Sampler = .mockKeepAll() let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .single ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) ) let headers = writer.traceHeaderFields XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1") } - func testItWritesMultipleHeader() { - let sampler: Sampler = .mockKeepAll() + func testWritingSampledTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .multiple + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "1") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") + } + + func testWritingDroppedTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .multiple ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField]) + XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0") + XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + } + + func testWritingSampledTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + injectEncoding: .multiple + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) ) let headers = writer.traceHeaderFields @@ -76,16 +169,19 @@ class B3HTTPHeadersWriterTests: XCTestCase { XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") } - func testItWritesMultipleHeaderWithSampling() { - let sampler: Sampler = .mockRejectAll() + func testWritingDroppedTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .custom(sampleRate: 0), injectEncoding: .multiple ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) ) let headers = writer.traceHeaderFields @@ -96,14 +192,17 @@ class B3HTTPHeadersWriterTests: XCTestCase { } func testItWritesMultipleHeaderWithoutOptionalValues() { - let sampler: Sampler = .mockKeepAll() let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .multiple ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) ) let headers = writer.traceHeaderFields diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift index a4af5ee6b4..880aa4f9d3 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift @@ -5,22 +5,25 @@ */ import XCTest +import TestUtilities @testable import DatadogInternal class HTTPHeadersReaderTests: XCTestCase { func testReadingSampledTraceContext() { - let writer = HTTPHeadersWriter(sampleRate: 100) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + writer.write(traceContext: .mockRandom()) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) } func testReadingNotSampledTraceContext() { - let writer = HTTPHeadersWriter(sampleRate: 0) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + writer.write(traceContext: .mockRandom()) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift new file mode 100644 index 0000000000..c104331c82 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift @@ -0,0 +1,83 @@ +/* + * 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 +import TestUtilities +import DatadogInternal + +class HTTPHeadersWriterTests: XCTestCase { + func testWritingSampledTraceContext_withAutoSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "1") + XCTAssertEqual(headers[TracingHTTPHeaders.traceIDField], "1234") + XCTAssertEqual(headers[TracingHTTPHeaders.parentSpanIDField], "2345") + XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") + } + + func testWritingDroppedTraceContext_withAutoSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "0") + XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) + } + + func testWritingSampledTraceContext_withCustomSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "1") + XCTAssertEqual(headers[TracingHTTPHeaders.traceIDField], "1234") + XCTAssertEqual(headers[TracingHTTPHeaders.parentSpanIDField], "2345") + XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") + } + + func testWritingDroppedTraceContext_withCustomSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "0") + XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift index dd6023423f..df81412ade 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift @@ -466,101 +466,23 @@ class NetworkInstrumentationFeatureTests: XCTestCase { ) } - // MARK: - URLRequest Interception + // MARK: - URLSessionTask Interception - func testGivenOpenTracing_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = HTTPHeadersWriter(sampler: .mockKeepAll()) - handler.firstPartyHosts = .init(["test.com": [.datadog]]) - handler.parentSpan = TraceContext(traceID: .mock(1, 1), spanID: .mock(2)) - - // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) - let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) - feature.flush() - - // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) - } - - func testGivenOpenTelemetry_b3single_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .single) - handler.firstPartyHosts = .init(["test.com": [.b3]]) - - // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3), parentSpanID: .mock(2)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) - let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) - feature.flush() - - // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) - } - - func testGivenOpenTelemetry_b3multi_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .multiple) - handler.firstPartyHosts = .init(["test.com": [.b3multi]]) - - // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3), parentSpanID: .mock(2)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) - let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) - feature.flush() - - // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) - } - - func testGivenW3C_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = W3CHTTPHeadersWriter( - sampler: .mockKeepAll(), - tracestate: [ - W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] - ) - handler.firstPartyHosts = .init(["test.com": [.tracecontext]]) - handler.parentSpan = TraceContext(traceID: .mock(1, 1), spanID: .mock(2)) + func testWhenInterceptingTaskWithMultipleTraceContexts_itTakesTheFirstContext() throws { + let traceContexts = [ + TraceContext(traceID: .mock(1, 1), spanID: .mock(2), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + TraceContext(traceID: .mock(2, 2), spanID: .mock(3), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + TraceContext(traceID: .mock(3, 3), spanID: .mock(4), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + ] // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) + feature.intercept(task: .mockAny(), with: traceContexts, additionalFirstPartyHosts: nil) feature.flush() // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) + let interception = try XCTUnwrap(handler.interceptions.first?.value) + XCTAssertEqual(interception.trace, traceContexts.first, "It should register first injected Trace Context") } // MARK: - First Party Hosts @@ -700,7 +622,7 @@ class NetworkInstrumentationFeatureTests: XCTestCase { closures: [ { feature.handlers = [self.handler] }, { _ = feature.intercept(request: requests.randomElement()!, additionalFirstPartyHosts: nil) }, - { feature.intercept(task: tasks.randomElement()!, additionalFirstPartyHosts: nil) }, + { feature.intercept(task: tasks.randomElement()!, with: [], additionalFirstPartyHosts: nil) }, { feature.task(tasks.randomElement()!, didReceive: .mockRandom()) }, { feature.task(tasks.randomElement()!, didFinishCollecting: .mockAny()) }, { feature.task(tasks.randomElement()!, didCompleteWithError: nil) }, diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift index 69f611e60c..6778803612 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift @@ -27,18 +27,20 @@ class W3CHTTPHeadersReaderTests: XCTestCase { } func testReadingSampledTraceContext() { - let writer = W3CHTTPHeadersWriter(sampleRate: 100) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + writer.write(traceContext: .mockRandom()) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) } func testReadingNotSampledTraceContext() { - let writer = W3CHTTPHeadersWriter(sampleRate: 0) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + writer.write(traceContext: .mockRandom()) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift index bcb45258f6..931714ca3e 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift @@ -5,35 +5,92 @@ */ import XCTest -@testable import DatadogInternal +import TestUtilities +import DatadogInternal class W3CHTTPHeadersWriterTests: XCTestCase { - func testW3CHTTPHeadersWriterWritesSingleHeader() { - let sampler: Sampler = .mockKeepAll() - let w3cHTTPHeadersWriter = W3CHTTPHeadersWriter( - sampler: sampler, + func testWritingSampledTraceContext_withAutoSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] ) - w3cHTTPHeadersWriter.write(traceID: .init(idHi: 1_234, idLo: 1_234), spanID: 2_345) - let headers = w3cHTTPHeadersWriter.traceHeaderFields + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-01") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") + } + + func testWritingDroppedTraceContext_withAutoSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .headBased, + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ] + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-00") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:0") + } + + func testWritingSampledTraceContext_withCustomSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ] + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-01") XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") } - func testW3CHTTPHeadersWriterWritesSingleHeaderWithSampling() { - let sampler: Sampler = .mockRejectAll() - let w3cHTTPHeadersWriter = W3CHTTPHeadersWriter( - sampler: sampler, + func testWritingDroppedTraceContext_withCustomSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] ) - w3cHTTPHeadersWriter.write(traceID: .init(idHi: 1_234, idLo: 1_234), spanID: 2_345, parentSpanID: 5_678) - let headers = w3cHTTPHeadersWriter.traceHeaderFields + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-00") XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:0") } diff --git a/DatadogObjc/Sources/Datadog+objc.swift b/DatadogObjc/Sources/Datadog+objc.swift index bc2ae6167e..da55a08285 100644 --- a/DatadogObjc/Sources/Datadog+objc.swift +++ b/DatadogObjc/Sources/Datadog+objc.swift @@ -70,11 +70,26 @@ public class DDDatadog: NSObject { Datadog.setUserInfo(id: id, name: name, email: email, extraInfo: castAttributesToSwift(extraInfo)) } + @objc + public static func addUserExtraInfo(_ extraInfo: [String: Any]) { + Datadog.addUserExtraInfo(castAttributesToSwift(extraInfo)) + } + @objc public static func setTrackingConsent(consent: DDTrackingConsent) { Datadog.set(trackingConsent: consent.sdkConsent) } + @objc + public static func isInitialized() -> Bool { + return Datadog.isInitialized() + } + + @objc + public static func stopInstance() { + Datadog.stopInstance() + } + @objc public static func clearAllData() { Datadog.clearAllData() diff --git a/DatadogObjc/Sources/DatadogConfiguration+objc.swift b/DatadogObjc/Sources/DatadogConfiguration+objc.swift index 9a9cd0b605..10377b319c 100644 --- a/DatadogObjc/Sources/DatadogConfiguration+objc.swift +++ b/DatadogObjc/Sources/DatadogConfiguration+objc.swift @@ -256,6 +256,13 @@ public class DDConfiguration: NSObject { set { sdkConfiguration.bundle = newValue } } + /// Sets additional configuration attributes. + /// This can be used to tweak internal features of the SDK and shouldn't be considered as a part of public API. + @objc public var additionalConfiguration: [String: Any] { + get { sdkConfiguration._internal.additionalConfiguration } + set { sdkConfiguration._internal_mutation { $0.additionalConfiguration = newValue } } + } + /// Creates a Datadog SDK Configuration object. /// /// - Parameters: diff --git a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift index 2bb596ecf3..b55ab262c9 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift @@ -37,7 +37,7 @@ public class DDB3HTTPHeadersWriter: NSObject { } @objc - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:injectEncoding:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init( samplingRate: Float, injectEncoding: DDInjectEncoding = .single @@ -46,12 +46,24 @@ public class DDB3HTTPHeadersWriter: NSObject { } @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public init( sampleRate: Float = 20, injectEncoding: DDInjectEncoding = .single ) { swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( - sampleRate: sampleRate, + samplingStrategy: .custom(sampleRate: sampleRate), + injectEncoding: .init(injectEncoding) + ) + } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy, + injectEncoding: DDInjectEncoding = .single + ) { + swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, injectEncoding: .init(injectEncoding) ) } diff --git a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift index 46e4b00ad0..24d759f25f 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift @@ -16,13 +16,23 @@ public class DDHTTPHeadersWriter: NSObject { } @objc - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate) } @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public init(sampleRate: Float = 20) { - swiftHTTPHeadersWriter = HTTPHeadersWriter(sampleRate: sampleRate) + swiftHTTPHeadersWriter = HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate) + ) + } + + @objc + public init(samplingStrategy: DDTraceSamplingStrategy) { + swiftHTTPHeadersWriter = HTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType + ) } } diff --git a/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift new file mode 100644 index 0000000000..fc1c5b081c --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift @@ -0,0 +1,37 @@ +/* + * 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 DatadogInternal + +/// Available strategies for sampling trace propagation headers. +@objc +public class DDTraceSamplingStrategy: NSObject { + internal let swiftType: DatadogInternal.TraceSamplingStrategy + + /// Trace propagation headers will be sampled same as propagated span. + /// + /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace + /// is determined from the first span of the trace, the head, when the trace is created. With `.headBased` + /// strategy, this decision is propagated through the request context to downstream services. + @objc + public static func headBased() -> DDTraceSamplingStrategy { + return DDTraceSamplingStrategy(swiftType: .headBased) + } + + /// Trace propagation headers will be sampled independently from sampling decision in propagated span. + /// + /// Use this option to apply the provided `sampleRate` for determining the decision to keep or drop the trace + /// in downstream services independently of sampling their parent span. + @objc + public static func custom(sampleRate: Float) -> DDTraceSamplingStrategy { + return DDTraceSamplingStrategy(swiftType: .custom(sampleRate: sampleRate)) + } + + private init(swiftType: DatadogInternal.TraceSamplingStrategy) { + self.swiftType = swiftType + } +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift index f7527cea7f..1ef16bc401 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift @@ -16,13 +16,27 @@ public class DDW3CHTTPHeadersWriter: NSObject { } @objc - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate) } @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public init(sampleRate: Float = 20) { - swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter(sampleRate: sampleRate, tracestate: [:]) + swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + tracestate: [:] + ) + } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy + ) { + swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, + tracestate: [:] + ) } } diff --git a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift index 516d4a746e..ab39b9c1cf 100644 --- a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift @@ -65,12 +65,8 @@ internal final class URLSessionRUMResourcesHandler: DatadogURLSessionHandler, RU // MARK: - DatadogURLSessionHandler - func modify(request: URLRequest, headerTypes: Set) -> URLRequest { - distributedTracing?.modify(request: request, headerTypes: headerTypes) ?? request - } - - func traceContext() -> DatadogInternal.TraceContext? { - nil // no-op + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { + distributedTracing?.modify(request: request, headerTypes: headerTypes) ?? (request, nil) } func interceptionDidStart(interception: DatadogInternal.URLSessionTaskInterception) { @@ -148,52 +144,57 @@ internal final class URLSessionRUMResourcesHandler: DatadogURLSessionHandler, RU } extension DistributedTracing { - func modify(request: URLRequest, headerTypes: Set) -> URLRequest { + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { let traceID = traceIDGenerator.generate() let spanID = spanIDGenerator.generate() + let injectedSpanContext = TraceContext( + traceID: traceID, + spanID: spanID, + parentSpanID: nil, + sampleRate: sampler.samplingRate, + isKept: sampler.sample() + ) var request = request + var hasSetAnyHeader = false headerTypes.forEach { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(sampler: sampler) + writer = HTTPHeadersWriter(samplingStrategy: .headBased) // To make sure the generated traces from RUM don’t affect APM Index Spans counts. request.setValue("rum", forHTTPHeaderField: TracingHTTPHeaders.originField) case .b3: writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, injectEncoding: .multiple ) case .tracecontext: writer = W3CHTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] ) } - writer.write( - traceID: traceID, - spanID: spanID, - parentSpanID: nil - ) + writer.write(traceContext: injectedSpanContext) writer.traceHeaderFields.forEach { field, value in // do not overwrite existing header if request.value(forHTTPHeaderField: field) == nil { + hasSetAnyHeader = true request.setValue(value, forHTTPHeaderField: field) } } } - return request + return (request, (hasSetAnyHeader && injectedSpanContext.isKept) ? injectedSpanContext : nil) } func trace(from interception: DatadogInternal.URLSessionTaskInterception) -> RUMSpanContext? { diff --git a/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift b/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift index a5f967cd26..a578ce340f 100644 --- a/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift +++ b/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift @@ -76,14 +76,16 @@ internal final class WebViewEventReceiver: FeatureMessageReceiver { let rum: RUMCoreContext = try rumBaggage.decode() var event = event - if let date = event["date"] as? Int { - let viewID = (event["view"] as? JSON)?["id"] as? String - let serverTimeOffsetInMs = self.getOffsetInMs(viewID: viewID, context: context) - let correctedDate = Int64(date) + serverTimeOffsetInMs + if + let date = event["date"] as? Int, + let view = event["view"] as? JSON, + let id = view["id"] as? String + { + let correctedDate = Int64(date) + self.offset(forView: id, context: context) event["date"] = correctedDate // Inject the container source and view id - if let viewID = self.viewCache.lastView(before: date, hasReplay: true) { + if let viewID = self.viewCache.lastView(before: correctedDate, hasReplay: true) { event[RUMViewEvent.CodingKeys.container.rawValue] = RUMViewEvent.Container( source: RUMViewEvent.Container.Source(rawValue: context.source) ?? .ios, view: RUMViewEvent.Container.View(id: viewID) @@ -98,13 +100,17 @@ internal final class WebViewEventReceiver: FeatureMessageReceiver { if var session = event["session"] as? JSON { session["id"] = rum.sessionID + // Unset `has_replay` if native replay is disabled + if context.hasReplay != true { + session["has_replay"] = context.hasReplay + } + event["session"] = session } - if var dd = event["_dd"] as? JSON { - var session = dd["session"] as? [String: Any] ?? [:] - session["plan"] = 1 - dd["session"] = session + if var dd = event["_dd"] as? JSON, context.hasReplay != true { + // Remove stats if native replay is disabled + dd["replay_stats"] = nil event["_dd"] = dd } @@ -149,28 +155,18 @@ internal final class WebViewEventReceiver: FeatureMessageReceiver { // MARK: - Time offsets - private typealias Offset = Int64 - private typealias ViewIDOffsetPair = (viewID: String, offset: Offset) - private var viewIDOffsetPairs = [ViewIDOffsetPair]() + private var offsets: [(id: String, value: Int64)] = [] - private func getOffsetInMs(viewID: String?, context: DatadogContext) -> Offset { - guard let viewID = viewID else { - return 0 + private func offset(forView id: String, context: DatadogContext) -> Int64 { + if let found = offsets.first(where: { $0.id == id }) { + return found.value } - purgeOffsets() - let found = viewIDOffsetPairs.first { $0.viewID == viewID } - if let found = found { - return found.offset - } let offset = context.serverTimeOffset.toInt64Milliseconds - viewIDOffsetPairs.insert((viewID: viewID, offset: offset), at: 0) - return offset - } + offsets.insert((id, offset), at: 0) + // only retain 3 offsets + offsets = Array(offsets.prefix(3)) - private func purgeOffsets() { - while viewIDOffsetPairs.count > 3 { - _ = viewIDOffsetPairs.popLast() - } + return offset } } diff --git a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift index 5ee6ee52af..021a3a1133 100644 --- a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift +++ b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift @@ -172,7 +172,9 @@ extension FrameInfoProvider { extension CADisplayLink: FrameInfoProvider { var maximumDeviceFramesPerSecond: Int { #if swift(>=5.9) && os(visionOS) - 120 + // Hardcoded as for now there's no good way of extracting maximum FPS on VisionOS + // https://developer.apple.com/documentation/visionos/analyzing-the-performance-of-your-visionos-app#Inspect-frame-rendering-performance + 90 #else UIScreen.main.maximumFramesPerSecond #endif diff --git a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift index b1c37a3f7e..d4d5a7c5aa 100644 --- a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift +++ b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift @@ -40,16 +40,23 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.datadog] ) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField), "rum") - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "64") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "100") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "_dd.p.tid=a") - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "64") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "100") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itInjectB3TraceHeaders() throws { @@ -64,13 +71,20 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000064-1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itInjectB3MultiTraceHeaders() throws { @@ -85,7 +99,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3multi] ) @@ -95,6 +109,13 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField), "0000000000000064") XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itInjectW3CTraceHeaders() throws { @@ -109,13 +130,20 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.tracecontext] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-01") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectDDTraceHeaders() throws { @@ -130,7 +158,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.datadog] ) @@ -139,6 +167,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField)) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectB3TraceHeaders() throws { @@ -153,13 +183,15 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "0") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectB3MultiTraceHeaders() throws { @@ -174,7 +206,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3multi] ) @@ -184,6 +216,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField)) XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "0") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectW3CTraceHeaders() throws { @@ -198,13 +232,15 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.tracecontext] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-00") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withSampledTrace_itDoesNotOverwriteTraceHeaders() throws { @@ -219,19 +255,21 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - var request: URLRequest = .mockWith(url: "https://www.example.com") - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) - request.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) - - request = handler.modify( - request: request, + var orgRequest: URLRequest = .mockWith(url: "https://www.example.com") + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.tagsField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.tracestate) + + let (request, traceContext) = handler.modify( + request: orgRequest, headerTypes: [ .datadog, .b3, @@ -241,6 +279,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "custom") @@ -249,6 +288,9 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "custom") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenTaskInterceptionWithNoSpanContext_whenInterceptionStarts_itStartsRUMResource() throws { @@ -298,7 +340,9 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { taskInterception.register(trace: TraceContext( traceID: 100, spanID: 200, - parentSpanID: nil + parentSpanID: nil, + sampleRate: .mockAny(), + isKept: .mockAny() )) XCTAssertNotNil(taskInterception.trace) @@ -475,7 +519,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) ) let request: URLRequest = .mockWith(httpMethod: "GET") - let modifiedRequest = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) + let (modifiedRequest, _) = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) XCTAssertEqual( modifiedRequest.allHTTPHeaderFields, @@ -486,8 +530,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { "X-B3-Sampled": "1", "X-B3-TraceId": "000000000000000a0000000000000064", "b3": "000000000000000a0000000000000064-0000000000000064-1", - "x-datadog-trace-id": "64", - "x-datadog-parent-id": "64", + "x-datadog-trace-id": "100", + "x-datadog-parent-id": "100", "x-datadog-sampling-priority": "1", "x-datadog-origin": "rum", "x-datadog-tags": "_dd.p.tid=a" diff --git a/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift b/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift index 0ddaa1fd7b..c740f49d7e 100644 --- a/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift +++ b/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift @@ -227,13 +227,11 @@ class WebViewEventReceiverTests: XCTestCase { // MARK: - Modifying Web Events - func testGivenRUMContextAvailable_whenReceivingWebEvent_itGetsEnrichedWithOtherMobileContextAndWritten() throws { + func testGivenRUMContextAvailable_whenReceivingWebEvent_itInjectRUMInfo() throws { // Given let dateProvider = RelativeDateProvider() let rumContext: RUMCoreContext = .mockRandom() featureScope.contextMock = .mockWith( - source: "react-native", - serverTimeOffset: .mockRandom(min: -10, max: 10).rounded(), baggages: [ RUMFeature.name: FeatureBaggage(rumContext) ] @@ -246,13 +244,6 @@ class WebViewEventReceiverTests: XCTestCase { viewCache: ViewCache(dateProvider: dateProvider) ) - let containerViewID: String = .mockRandom() - receiver.viewCache.insert( - id: containerViewID, - timestamp: dateProvider.now.timeIntervalSince1970.toInt64Milliseconds, - hasReplay: true - ) - dateProvider.advance(bySeconds: 1) let date = dateProvider.now.timeIntervalSince1970.toInt64Milliseconds let random = mockRandomAttributes() // because below we only mock partial web event, we use this random to make the test fuzzy @@ -260,7 +251,9 @@ class WebViewEventReceiverTests: XCTestCase { // Known properties: "_dd": ["browser_sdk_version": "5.2.0"], "application": ["id": String.mockRandom()], - "session": ["id": String.mockRandom()], + "session": [ + "id": String.mockRandom(), + ], "view": ["id": "00000000-aaaa-0000-aaaa-000000000000"], "date": Int(date), ].merging(random, uniquingKeysWith: { old, _ in old }) @@ -273,15 +266,12 @@ class WebViewEventReceiverTests: XCTestCase { let expectedWebEventWritten: JSON = [ // Known properties: "_dd": [ - "session": ["plan": 1], "browser_sdk_version": "5.2.0" ] as [String: Any], - "container": [ - "source": "react-native", - "view": [ "id": containerViewID ] - ] as [String: Any], "application": ["id": rumContext.applicationID], - "session": ["id": rumContext.sessionID], + "session": [ + "id": rumContext.sessionID, + ], "view": ["id": "00000000-aaaa-0000-aaaa-000000000000"], "date": date + featureScope.contextMock.serverTimeOffset.toInt64Milliseconds, ].merging(random, uniquingKeysWith: { old, _ in old }) @@ -338,4 +328,162 @@ class WebViewEventReceiverTests: XCTestCase { let errorTelemetry = try XCTUnwrap(featureScope.telemetryMock.messages.firstError(), "It must send error telemetry") XCTAssertTrue(errorTelemetry.message.hasPrefix("Failed to decode `RUMCoreContext`")) } + + func testGivenReplayContextAvailable_whenReceivingWebEvent_itInjectReplayInfo() throws { + // Given + let dateProvider = RelativeDateProvider() + let rumContext: RUMCoreContext = .mockRandom() + featureScope.contextMock = .mockWith( + source: "react-native", + baggages: [ + RUMFeature.name: FeatureBaggage(rumContext), + SessionReplayDependency.hasReplay: FeatureBaggage(true) + ] + ) + + let receiver = WebViewEventReceiver( + featureScope: featureScope, + dateProvider: DateProviderMock(), + commandSubscriber: RUMCommandSubscriberMock(), + viewCache: ViewCache(dateProvider: dateProvider) + ) + + let containerViewID: String = .mockRandom() + receiver.viewCache.insert( + id: containerViewID, + timestamp: dateProvider.now.timeIntervalSince1970.toInt64Milliseconds, + hasReplay: true + ) + + dateProvider.advance(bySeconds: 1) + let date = dateProvider.now.timeIntervalSince1970.toInt64Milliseconds + let random = mockRandomAttributes() // because below we only mock partial web event, we use this random to make the test fuzzy + let webHasReplay: Bool = .mockRandom() + let webEventMock: JSON = [ + // Known properties: + "_dd": [ + "browser_sdk_version": "5.2.0", + "replay_stats": RUMViewEvent.DD.ReplayStats( + recordsCount: 10, + segmentsCount: 1, + segmentsTotalRawSize: 10 + ) + ] as [String: Any], + "application": ["id": String.mockRandom()], + "session": [ + "id": String.mockRandom(), + "has_replay": webHasReplay + ] as [String: Any], + "view": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "date": Int(date), + ].merging(random, uniquingKeysWith: { old, _ in old }) + + // When + let result = receiver.receive(message: webViewTrackingMessage(with: webEventMock), from: NOPDatadogCore()) + + // Then + let expectedWebEventWritten: JSON = [ + // Known properties: + "_dd": [ + "browser_sdk_version": "5.2.0", + "replay_stats": [ + "records_count": 10, + "segments_count": 1, + "segments_total_raw_size": 10 + ] + ] as [String: Any], + "container": [ + "source": "react-native", + "view": [ "id": containerViewID ] + ] as [String: Any], + "application": ["id": rumContext.applicationID], + "session": [ + "id": rumContext.sessionID, + "has_replay": webHasReplay + ] as [String: Any], + "view": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "date": date + featureScope.contextMock.serverTimeOffset.toInt64Milliseconds, + ].merging(random, uniquingKeysWith: { old, _ in old }) + + XCTAssertTrue(result, "It must accept the message") + XCTAssertEqual(featureScope.eventsWritten.count, 1, "It must write web event to core") + let actualWebEventWritten = try XCTUnwrap(featureScope.eventsWritten.first) + DDAssertJSONEqual(AnyCodable(actualWebEventWritten), AnyCodable(expectedWebEventWritten)) + } + + func testGivenReplayContextNotAvailable_whenReceivingWebEvent_itRemovesReplayInfo() throws { + // Given + let dateProvider = RelativeDateProvider() + let rumContext: RUMCoreContext = .mockRandom() + featureScope.contextMock = .mockWith( + source: "react-native", + baggages: [ + RUMFeature.name: FeatureBaggage(rumContext), + SessionReplayDependency.hasReplay: FeatureBaggage(false) + ] + ) + + let receiver = WebViewEventReceiver( + featureScope: featureScope, + dateProvider: DateProviderMock(), + commandSubscriber: RUMCommandSubscriberMock(), + viewCache: ViewCache(dateProvider: dateProvider) + ) + + let containerViewID: String = .mockRandom() + receiver.viewCache.insert( + id: containerViewID, + timestamp: dateProvider.now.timeIntervalSince1970.toInt64Milliseconds, + hasReplay: true + ) + + dateProvider.advance(bySeconds: 1) + let date = dateProvider.now.timeIntervalSince1970.toInt64Milliseconds + let random = mockRandomAttributes() // because below we only mock partial web event, we use this random to make the test fuzzy + let webEventMock: JSON = [ + // Known properties: + "_dd": [ + "browser_sdk_version": "5.2.0", + "replay_stats": RUMViewEvent.DD.ReplayStats( + recordsCount: .mockRandom(), + segmentsCount: .mockRandom(), + segmentsTotalRawSize: .mockRandom() + ) + ] as [String: Any], + "application": ["id": String.mockRandom()], + "session": [ + "id": String.mockRandom(), + "has_replay": true + ] as [String: Any], + "view": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "date": Int(date), + ].merging(random, uniquingKeysWith: { old, _ in old }) + + // When + let result = receiver.receive(message: webViewTrackingMessage(with: webEventMock), from: NOPDatadogCore()) + + // Then + let expectedWebEventWritten: JSON = [ + // Known properties: + "_dd": [ + "browser_sdk_version": "5.2.0" + ] as [String: Any], + "container": [ + "source": "react-native", + "view": [ "id": containerViewID ] + ] as [String: Any], + "application": ["id": rumContext.applicationID], + "session": [ + "id": rumContext.sessionID, + "has_replay": false + ] as [String: Any], + "view": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "date": date + featureScope.contextMock.serverTimeOffset.toInt64Milliseconds, + ].merging(random, uniquingKeysWith: { old, _ in old }) + + XCTAssertTrue(result, "It must accept the message") + XCTAssertEqual(featureScope.eventsWritten.count, 1, "It must write web event to core") + let actualWebEventWritten = try XCTUnwrap(featureScope.eventsWritten.first) + DDAssertJSONEqual(AnyCodable(actualWebEventWritten), AnyCodable(expectedWebEventWritten)) + } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/UnsupportedViews.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/UnsupportedViews.storyboard index 4818923400..b3e69d1854 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/UnsupportedViews.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/UnsupportedViews.storyboard @@ -1,9 +1,9 @@ - + - + @@ -26,40 +26,23 @@ - - + - - - - - - - - - - - - + @@ -70,23 +53,18 @@ - + - - - - - diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testUnsupportedView()-allowAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testUnsupportedView()-allowAll-privacy.png.json index 9cf7aaa02d..6ccaf04733 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testUnsupportedView()-allowAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testUnsupportedView()-allowAll-privacy.png.json @@ -1 +1 @@ -{"hash":"60542c413a1bfb3875b961f0a2e50490a30ffc87"} \ No newline at end of file +{"hash":"8c803bf8fe88005af09b838159af0d517170090b"} \ No newline at end of file diff --git a/DatadogSessionReplay/Sources/Feature/Baggages.swift b/DatadogSessionReplay/Sources/Feature/Baggages.swift index de0ee4ef55..7f331740ff 100644 --- a/DatadogSessionReplay/Sources/Feature/Baggages.swift +++ b/DatadogSessionReplay/Sources/Feature/Baggages.swift @@ -19,7 +19,7 @@ internal enum RUMDependency { } /// The RUM context received from `DatadogCore`. -internal struct RUMContext: Decodable, Equatable { +internal struct RUMContext: Codable, Equatable { static let key = "rum" enum CodingKeys: String, CodingKey { diff --git a/DatadogSessionReplay/Sources/Feature/RUMContextReceiver.swift b/DatadogSessionReplay/Sources/Feature/RUMContextReceiver.swift index c8fcdc0f67..25d7e77f3b 100644 --- a/DatadogSessionReplay/Sources/Feature/RUMContextReceiver.swift +++ b/DatadogSessionReplay/Sources/Feature/RUMContextReceiver.swift @@ -35,7 +35,7 @@ internal class RUMContextReceiver: FeatureMessageReceiver, RUMContextObserver { do { // Extract the `RUMContext` or `nil` if RUM session is not sampled: - new = try context.baggages[RUMContext.key].map { try $0.decode() } + new = try context.baggages[RUMContext.key]?.decode() } catch { core.telemetry .error("Fails to decode RUM context from Session Replay", error: error) diff --git a/DatadogSessionReplay/Sources/Feature/RequestBuilders/JSON/SegmentJSON.swift b/DatadogSessionReplay/Sources/Feature/RequestBuilders/JSON/SegmentJSON.swift index 541e97fe38..11d6139fe0 100644 --- a/DatadogSessionReplay/Sources/Feature/RequestBuilders/JSON/SegmentJSON.swift +++ b/DatadogSessionReplay/Sources/Feature/RequestBuilders/JSON/SegmentJSON.swift @@ -16,7 +16,7 @@ internal typealias JSONObject = [String: Any] /// Can be considered a temporary solution until we find a way to decode `[SRRecords]` unambiguously /// through `Codable` interface. internal struct SegmentJSON { - private enum Constants { + enum Constants { /// The `timestamp` is common to all records. /// see. https://github.com/DataDog/rum-events-format/blob/master/schemas/session-replay/common/_common-record-schema.json#L9 static let timestampKey = "timestamp" diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index 86a9ce45c6..389b095f9b 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -44,13 +44,19 @@ internal class SessionReplayFeature: DatadogRemoteFeature { additionalNodeRecorders: configuration._additionalNodeRecorders ) let scheduler = MainThreadScheduler(interval: 0.1) - let messageReceiver = RUMContextReceiver() + let contextReceiver = RUMContextReceiver() + + self.messageReceiver = CombinedFeatureMessageReceiver([ + contextReceiver, + WebViewRecordReceiver( + scope: core.scope(for: SessionReplayFeature.self) + ) + ]) - self.messageReceiver = messageReceiver self.recordingCoordinator = RecordingCoordinator( scheduler: scheduler, privacy: configuration.defaultPrivacyLevel, - rumContextObserver: messageReceiver, + rumContextObserver: contextReceiver, srContextPublisher: SRContextPublisher(core: core), recorder: recorder, sampler: Sampler(samplingRate: configuration.debugSDK ? 100 : configuration.replaySampleRate) diff --git a/DatadogSessionReplay/Sources/Feature/WebViewRecordReceiver.swift b/DatadogSessionReplay/Sources/Feature/WebViewRecordReceiver.swift new file mode 100644 index 0000000000..164505185f --- /dev/null +++ b/DatadogSessionReplay/Sources/Feature/WebViewRecordReceiver.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 Foundation +import DatadogInternal + +internal struct WebViewRecordReceiver: FeatureMessageReceiver { + internal struct WebRecord: Encodable { + /// The RUM application ID of all records. + let applicationID: String + /// The RUM session ID of all records. + let sessionID: String + /// The RUM view ID of all records. + let viewID: String + /// Records enriched with further information. + let records: [AnyEncodable] + } + + /// Session Replay feature scope. + let scope: FeatureScope + + func receive(message: DatadogInternal.FeatureMessage, from core: DatadogInternal.DatadogCoreProtocol) -> Bool { + guard case let .webview(.record(event, view)) = message else { + return false + } + + scope.eventWriteContext { context, writer in + do { + // Extract the `RUMContext` or `nil` if RUM session is not sampled: + guard let rumContext = try context.baggages[RUMContext.key]?.decode(type: RUMContext.self) else { + return + } + + var event = event + + if let timestamp = event["timestamp"] as? Int, let offset = rumContext.viewServerTimeOffset { + event["timestamp"] = Int64(timestamp) + offset.toInt64Milliseconds + } + + let record = WebRecord( + applicationID: rumContext.applicationID, + sessionID: rumContext.sessionID, + viewID: view.id, + records: [AnyEncodable(event)] + ) + + writer.write(value: record) + } catch { + core.telemetry + .error("Fails to decode RUM context from Session Replay", error: error) + return + } + } + + return true + } +} diff --git a/DatadogSessionReplay/Sources/Models/SRDataModels.swift b/DatadogSessionReplay/Sources/Models/SRDataModels.swift index 61a2806adc..fbc7613f95 100644 --- a/DatadogSessionReplay/Sources/Models/SRDataModels.swift +++ b/DatadogSessionReplay/Sources/Models/SRDataModels.swift @@ -452,6 +452,9 @@ public struct SRWebviewWireframe: Codable, Hashable { /// Defines the unique ID of the wireframe. This is persistent throughout the view lifetime. public let id: Int64 + /// Whether this web-view is visible or not. + public let isVisible: Bool? + /// The style of this wireframe. public let shapeStyle: SRShapeStyle? @@ -475,6 +478,7 @@ public struct SRWebviewWireframe: Codable, Hashable { case clip = "clip" case height = "height" case id = "id" + case isVisible = "isVisible" case shapeStyle = "shapeStyle" case slotId = "slotId" case type = "type" @@ -970,6 +974,9 @@ public struct SRIncrementalSnapshotRecord: Codable { /// Defines the unique ID of the wireframe. This is persistent throughout the view lifetime. public let id: Int64 + /// Whether this web-view is visible or not. + public let isVisible: Bool? + /// The style of this wireframe. public let shapeStyle: SRShapeStyle? @@ -993,6 +1000,7 @@ public struct SRIncrementalSnapshotRecord: Codable { case clip = "clip" case height = "height" case id = "id" + case isVisible = "isVisible" case shapeStyle = "shapeStyle" case slotId = "slotId" case type = "type" @@ -1322,4 +1330,4 @@ public enum SRRecord: Codable { } } #endif -// Generated from https://github.com/DataDog/rum-events-format/tree/c3747b3facf75e51cbad4c32f77ec3894f5a7249 +// Generated from https://github.com/DataDog/rum-events-format/tree/be033e3251da4a20891a774f9843c489a693c80d diff --git a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/RecordsBuilder.swift b/DatadogSessionReplay/Sources/Processor/Builders/RecordsBuilder.swift similarity index 100% rename from DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/RecordsBuilder.swift rename to DatadogSessionReplay/Sources/Processor/Builders/RecordsBuilder.swift diff --git a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift similarity index 86% rename from DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift rename to DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift index 993167f904..b25cbfef09 100644 --- a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift @@ -20,6 +20,23 @@ public typealias WireframeID = NodeID /// Note: `WireframesBuilder` is used by `Processor` on a single background thread. @_spi(Internal) public class SessionReplayWireframesBuilder { + /// The cache of webview slot IDs in memory during snapshot. + private var webViewSlotIDs: Set + + /// Creates a builder for builder wireframes in snapshot processing. + /// + /// The builder takes optional webview slot IDs in cache that can be updated + /// while traversing the node. The cache will be used to create wireframes + /// that are not visible be still need to be kept by the player. + /// + /// - Parameter webviewSlotIDs: The webview slot IDs in memory during snapshot. + init(webViewSlotIDs: Set = []) { + self.webViewSlotIDs = webViewSlotIDs + } +} + +@_spi(Internal) +extension SessionReplayWireframesBuilder { /// A set of fallback values to use if the actual value cannot be read or converted. /// /// The idea is to always provide value, which would make certain element visible in the player. @@ -177,10 +194,9 @@ public class SessionReplayWireframesBuilder { return .placeholderWireframe(value: wireframe) } - public func createWebViewWireframe( - id: Int64, + public func visibleWebViewWireframe( + id: Int, frame: CGRect, - slotId: String, clip: SRContentClip? = nil, borderColor: CGColor? = nil, borderWidth: CGFloat? = nil, @@ -192,17 +208,41 @@ public class SessionReplayWireframesBuilder { border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth), clip: clip, height: Int64(withNoOverflow: frame.height), - id: id, + id: Int64(id), + isVisible: true, shapeStyle: createShapeStyle(backgroundColor: backgroundColor, cornerRadius: cornerRadius, opacity: opacity), - slotId: slotId, + slotId: String(id), width: Int64(withNoOverflow: frame.size.width), x: Int64(withNoOverflow: frame.minX), y: Int64(withNoOverflow: frame.minY) ) + /// Remove the slot from the builder because a wireframe + /// has been created. + webViewSlotIDs.remove(id) return .webviewWireframe(value: wireframe) } + internal func hiddenWebViewWireframes() -> [SRWireframe] { + defer { webViewSlotIDs.removeAll() } + return webViewSlotIDs.map { id in + let wireframe = SRWebviewWireframe( + border: nil, + clip: nil, + height: 0, + id: Int64(id), + isVisible: false, + shapeStyle: nil, + slotId: String(id), + width: 0, + x: 0, + y: 0 + ) + + return .webviewWireframe(value: wireframe) + } + } + // MARK: - Private private func createShapeBorder(borderColor: CGColor?, borderWidth: CGFloat?) -> SRShapeBorder? { diff --git a/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift b/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift index 032ec09dc7..c5e197ba1a 100644 --- a/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift +++ b/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift @@ -35,6 +35,8 @@ extension SRWireframe: Diffable { return this.hashValue != other.hashValue case let (.placeholderWireframe(this), .placeholderWireframe(other)): return this.hashValue != other.hashValue + case let (.webviewWireframe(this), .webviewWireframe(other)): + return this.hashValue != other.hashValue default: return true } @@ -194,6 +196,7 @@ extension SRWebviewWireframe: MutableWireframe { clip: use(clip, ifDifferentThan: other.clip), height: use(height, ifDifferentThan: other.height), id: id, + isVisible: use(isVisible, ifDifferentThan: other.isVisible), shapeStyle: use(shapeStyle, ifDifferentThan: other.shapeStyle), slotId: slotId, width: use(width, ifDifferentThan: other.width), diff --git a/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift index d2d136173c..304a153fce 100644 --- a/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift @@ -33,8 +33,6 @@ internal protocol SnapshotProcessing { internal class SnapshotProcessor: SnapshotProcessing { /// Flattens VTS received from `Recorder` by removing invisible nodes. private let nodesFlattener = NodesFlattener() - /// Builds SR wireframes to describe UI elements. - private let wireframesBuilder = WireframesBuilder() /// Builds SR records to transport SR wireframes. private let recordsBuilder: RecordsBuilder @@ -78,10 +76,16 @@ internal class SnapshotProcessor: SnapshotProcessing { } private func processSync(viewTreeSnapshot: ViewTreeSnapshot, touchSnapshot: TouchSnapshot?) { - let flattenedNodes = nodesFlattener.flattenNodes(in: viewTreeSnapshot) - let wireframes: [SRWireframe] = flattenedNodes - .map { node in node.wireframesBuilder } - .flatMap { nodeBuilder in nodeBuilder.buildWireframes(with: wireframesBuilder) } + let builder = WireframesBuilder(webViewSlotIDs: viewTreeSnapshot.webViewSlotIDs) + let nodes = nodesFlattener.flattenNodes(in: viewTreeSnapshot) + + // build wireframe from nodes + var wireframes: [SRWireframe] = nodes.flatMap { node in + node.wireframesBuilder.buildWireframes(with: builder) + } + + // build hidden webview wireframes and place them at the beginning + wireframes = builder.hiddenWebViewWireframes() + wireframes interceptWireframes?(wireframes) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift index ace25c2317..08edf2e978 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift @@ -17,7 +17,6 @@ internal struct UnsupportedViewRecorder: NodeRecorder { { _, context in context.viewControllerContext.isRootView(of: .activity) }, { _, context in context.viewControllerContext.isRootView(of: .swiftUI) }, { view, _ in view is UIProgressView }, - { view, _ in view is WKWebView }, { view, _ in view is UIActivityIndicatorView } ] // swiftlint:enable opening_brace diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift index 0f4e48aac2..ee183805fa 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift @@ -16,38 +16,34 @@ internal class WKWebViewRecorder: NodeRecorder { return nil } - guard attributes.isVisible else { - return InvisibleElement.constant - } - - let builder = WKWebViewWireframesBuilder( - wireframeID: context.ids.nodeID(view: view, nodeRecorder: self), - slotID: webView.configuration.userContentController.hash, - attributes: attributes - ) + // Add the webview to cache + context.webViewCache.add(webView) + let builder = WKWebViewWireframesBuilder(slotID: webView.hash, attributes: attributes) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) } } internal struct WKWebViewWireframesBuilder: NodeWireframesBuilder { - let wireframeID: WireframeID - /// The slot identifier of the webview controller. + /// The webview slot ID. let slotID: Int - /// Attributes of the `UIView`. + let attributes: ViewAttributes - var wireframeRect: CGRect { - attributes.frame - } + var wireframeRect: CGRect { attributes.frame } func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { + guard attributes.isVisible else { + // ignore hidden webview, the wireframes will be built + // for hidden slot + return [] + } + return [ - builder.createWebViewWireframe( - id: wireframeID, - frame: wireframeRect, - slotId: String(slotID), + builder.visibleWebViewWireframe( + id: slotID, + frame: attributes.frame, borderColor: attributes.layerBorderColor, borderWidth: attributes.layerBorderWidth, backgroundColor: attributes.backgroundColor, @@ -57,4 +53,5 @@ internal struct WKWebViewWireframesBuilder: NodeWireframesBuilder { ] } } + #endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift index 46b5b1c7e5..a98d1a0910 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift @@ -8,6 +8,7 @@ import Foundation import SafariServices import SwiftUI +import WebKit /// The context of recording subtree hierarchy. /// @@ -22,6 +23,8 @@ public struct SessionReplayViewTreeRecordingContext { public let ids: NodeIDGenerator /// Variable view controller related context var viewControllerContext: ViewControllerContext = .init() + /// Webviews caching. + let webViewCache: NSHashTable } // This alias enables us to have a more unique name exposed through public-internal access level diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index abf5c5fd30..0f1f7b35b6 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -27,6 +27,8 @@ internal struct ViewTreeSnapshot { let nodes: [Node] /// An array of resource references recorded for this snapshot - sequenced in DFS order. let resources: [Resource] + /// A set of webview slot IDs recorded for this and past snapshots. + let webViewSlotIDs: Set } /// An individual node in `ViewTreeSnapshot`. A `SessionReplayNode` describes a single view - similar: an array of nodes describes diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index fd0e65f8b0..89e2e56075 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -7,6 +7,7 @@ #if os(iOS) import Foundation import UIKit +import WebKit /// Builds `ViewTreeSnapshot` for given root view. /// @@ -16,6 +17,8 @@ internal struct ViewTreeSnapshotBuilder { let viewTreeRecorder: ViewTreeRecorder /// Generates stable IDs for traversed views. let idsGenerator: NodeIDGenerator + /// The webviews cache. + let webViewCache: NSHashTable = .weakObjects() /// Builds the `ViewTreeSnapshot` for given root view. /// @@ -28,7 +31,8 @@ internal struct ViewTreeSnapshotBuilder { let context = ViewTreeRecordingContext( recorder: recorderContext, coordinateSpace: rootView, - ids: idsGenerator + ids: idsGenerator, + webViewCache: webViewCache ) let recording = viewTreeRecorder.record(rootView, in: context) let snapshot = ViewTreeSnapshot( @@ -36,7 +40,8 @@ internal struct ViewTreeSnapshotBuilder { context: recorderContext, viewportSize: rootView.bounds.size, nodes: recording.nodes, - resources: recording.resources + resources: recording.resources, + webViewSlotIDs: Set(webViewCache.allObjects.map(\.hash)) ) return snapshot } @@ -68,6 +73,7 @@ internal func createDefaultNodeRecorders() -> [NodeRecorder] { UITabBarRecorder(), UIPickerViewRecorder(), UIDatePickerRecorder(), + WKWebViewRecorder() ] } #endif diff --git a/DatadogSessionReplay/Tests/Feature/RUMContextReceiverTests.swift b/DatadogSessionReplay/Tests/Feature/RUMContextReceiverTests.swift index e89792597e..f72de77d05 100644 --- a/DatadogSessionReplay/Tests/Feature/RUMContextReceiverTests.swift +++ b/DatadogSessionReplay/Tests/Feature/RUMContextReceiverTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import TestUtilities import DatadogInternal @@ -242,3 +243,4 @@ class RUMContextReceiverTests: XCTestCase { waitForExpectations(timeout: 0.1) } } +#endif diff --git a/DatadogSessionReplay/Tests/Feature/RequestBuilder/JSON/SegmentJSONTests.swift b/DatadogSessionReplay/Tests/Feature/RequestBuilder/JSON/SegmentJSONTests.swift index 9178928af9..bc4b6e4d93 100644 --- a/DatadogSessionReplay/Tests/Feature/RequestBuilder/JSON/SegmentJSONTests.swift +++ b/DatadogSessionReplay/Tests/Feature/RequestBuilder/JSON/SegmentJSONTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import TestUtilities import DatadogInternal @@ -181,3 +182,4 @@ class SegmentJSONTests: XCTestCase { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Feature/RequestBuilder/Multipart/MultipartFormDataTests.swift b/DatadogSessionReplay/Tests/Feature/RequestBuilder/Multipart/MultipartFormDataTests.swift index 25ed57a054..bf3c68ff22 100644 --- a/DatadogSessionReplay/Tests/Feature/RequestBuilder/Multipart/MultipartFormDataTests.swift +++ b/DatadogSessionReplay/Tests/Feature/RequestBuilder/Multipart/MultipartFormDataTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -62,3 +63,4 @@ class MultipartFormDataTests: XCTestCase { XCTAssertEqual(expectedDataString, actualDataString) } } +#endif diff --git a/DatadogSessionReplay/Tests/Feature/RequestBuilder/ResourceRequestBuilderTests.swift b/DatadogSessionReplay/Tests/Feature/RequestBuilder/ResourceRequestBuilderTests.swift index fb91980bf8..c578a7afbf 100644 --- a/DatadogSessionReplay/Tests/Feature/RequestBuilder/ResourceRequestBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Feature/RequestBuilder/ResourceRequestBuilderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import DatadogInternal @testable import DatadogSessionReplay @@ -156,3 +157,4 @@ class ResourceRequestBuilderTests: XCTestCase { XCTAssertThrowsError(try builder.request(for: [.mockWith(data: "abc".utf8Data)], with: .mockRandom())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift b/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift index e37e6b158d..d468db3e88 100644 --- a/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import DatadogInternal @@ -255,3 +256,4 @@ class SegmentRequestBuilderTests: XCTestCase { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Feature/SRContextPublisherTests.swift b/DatadogSessionReplay/Tests/Feature/SRContextPublisherTests.swift index cc878d91a9..482846061d 100644 --- a/DatadogSessionReplay/Tests/Feature/SRContextPublisherTests.swift +++ b/DatadogSessionReplay/Tests/Feature/SRContextPublisherTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay import TestUtilities @@ -56,3 +57,4 @@ private extension PassthroughCoreMock { try? context.baggages["sr_records_count_by_view_id"]?.decode() } } +#endif diff --git a/DatadogSessionReplay/Tests/Feature/WebViewRecordReceiverTests.swift b/DatadogSessionReplay/Tests/Feature/WebViewRecordReceiverTests.swift new file mode 100644 index 0000000000..0d6d081604 --- /dev/null +++ b/DatadogSessionReplay/Tests/Feature/WebViewRecordReceiverTests.swift @@ -0,0 +1,118 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogInternal +@testable import DatadogSessionReplay + +class WebViewRecordReceiverTests: XCTestCase { + func testGivenRUMContextAvailable_whenReceivingWebRecord_itCreatesSegment() throws { + let serverTimeOffset: TimeInterval = .mockRandom(min: -10, max: 10).rounded() + + let rumContext: RUMContext = .mockWith( + serverTimeOffset: serverTimeOffset + ) + + let scope = FeatureScopeMock( + context: .mockWith( + source: "react-native", + baggages: ["rum": FeatureBaggage(rumContext)] + ) + ) + + // Given + let receiver = WebViewRecordReceiver( + scope: scope + ) + + let random = mockRandomAttributes() // because below we only mock partial web event, we use this random to make the test fuzzy + let webRecordMock: [String: Any] = [ + "timestamp": 100_000, + "type": 2 + ].merging(random, uniquingKeysWith: { old, _ in old }) + + let browserViewID: String = .mockRandom() + + // When + + let message = WebViewMessage.record(webRecordMock, WebViewMessage.View(id: browserViewID)) + let result = receiver.receive(message: .webview(message), from: NOPDatadogCore()) + + // Then + let expectedWebSegmentWritten: [String: Any] = [ + "applicationID": rumContext.applicationID, + "sessionID": rumContext.sessionID, + "viewID": browserViewID, + "records": [ + [ + "timestamp": 100_000 + serverTimeOffset.toInt64Milliseconds, + "type": 2 + ].merging(random, uniquingKeysWith: { old, _ in old }) + ] + ] + + XCTAssertTrue(result, "It must accept the message") + XCTAssertEqual(scope.eventsWritten.count, 1, "It must write web segment to core") + let actualWebEventWritten = try XCTUnwrap(scope.eventsWritten.first) + DDAssertJSONEqual(AnyCodable(actualWebEventWritten), AnyCodable(expectedWebSegmentWritten)) + } + + func testGivenRUMContextNotAvailable_whenReceivingWebRecord_itIsDropped() throws { + let scope = FeatureScopeMock() + + // Given + XCTAssertNil(scope.contextMock.baggages["rum"]) + + let receiver = WebViewRecordReceiver(scope: scope) + + // When + let record = WebViewMessage.record(mockRandomAttributes(), WebViewMessage.View(id: .mockRandom())) + let result = receiver.receive(message: .webview(record), from: NOPDatadogCore()) + + // Then + XCTAssertTrue(result, "It must accept the message") + XCTAssertTrue(scope.eventsWritten.isEmpty, "The event must be dropped") + } + + func testWhenReceivingOtherMessage_itRejectsIt() throws { + let scope = FeatureScopeMock() + + // Given + let receiver = WebViewRecordReceiver(scope: scope) + + // When + let otherMessage: FeatureMessage = .baggage(key: "message to other receiver", value: String.mockRandom()) + let result = receiver.receive(message: otherMessage, from: NOPDatadogCore()) + + // Then + XCTAssertFalse(result, "It must reject messages addressed to other receivers") + } + + func testWhenReceivingInvalidBaggage_itSendsTelemetryError() throws { + // Given + let telemetry = TelemetryReceiverMock() + let scope = FeatureScopeMock( + context: .mockWith(baggages: ["rum": FeatureBaggage(123)]) + ) + let core = PassthroughCoreMock( + messageReceiver: telemetry + ) + + let receiver = WebViewRecordReceiver(scope: scope) + + // When + let record = WebViewMessage.record(mockRandomAttributes(), WebViewMessage.View(id: .mockRandom())) + XCTAssert( + receiver.receive(message: .webview(record), from: core) + ) + + // Then + let message = try XCTUnwrap(telemetry.messages.first?.asError?.message) + XCTAssert(message.contains("Fails to decode RUM context from Session Replay - typeMismatch")) + } +} diff --git a/DatadogSessionReplay/Tests/Mocks/MultipartBuilderSpy.swift b/DatadogSessionReplay/Tests/Mocks/MultipartBuilderSpy.swift index 66425ef305..79a31f29af 100644 --- a/DatadogSessionReplay/Tests/Mocks/MultipartBuilderSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/MultipartBuilderSpy.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @testable import DatadogSessionReplay @@ -22,3 +23,4 @@ class MultipartBuilderSpy: MultipartFormDataBuilder { func build() -> Data { returnedData } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift b/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift index 7b399f333b..90a80065a0 100644 --- a/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @testable import DatadogSessionReplay @@ -13,3 +14,4 @@ internal class NoQueue: Queue { block() } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/RUMContextMocks.swift b/DatadogSessionReplay/Tests/Mocks/RUMContextMocks.swift new file mode 100644 index 0000000000..e4e5b58f9e --- /dev/null +++ b/DatadogSessionReplay/Tests/Mocks/RUMContextMocks.swift @@ -0,0 +1,39 @@ +/* + * 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 TestUtilities + +@testable import DatadogSessionReplay + +extension RUMContext: AnyMockable, RandomMockable { + public static func mockAny() -> RUMContext { + return .mockWith() + } + + public static func mockRandom() -> RUMContext { + return RUMContext( + applicationID: .mockRandom(), + sessionID: .mockRandom(), + viewID: .mockRandom(), + viewServerTimeOffset: .mockRandom() + ) + } + + static func mockWith( + applicationID: String = .mockAny(), + sessionID: String = .mockAny(), + viewID: String? = .mockAny(), + serverTimeOffset: TimeInterval = .mockAny() + ) -> RUMContext { + return RUMContext( + applicationID: applicationID, + sessionID: sessionID, + viewID: viewID, + viewServerTimeOffset: serverTimeOffset + ) + } +} diff --git a/DatadogSessionReplay/Tests/Mocks/RUMContextObserverMock.swift b/DatadogSessionReplay/Tests/Mocks/RUMContextObserverMock.swift index a2719887a3..69e42a051a 100644 --- a/DatadogSessionReplay/Tests/Mocks/RUMContextObserverMock.swift +++ b/DatadogSessionReplay/Tests/Mocks/RUMContextObserverMock.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @testable import DatadogSessionReplay @@ -20,3 +21,4 @@ class RUMContextObserverMock: RUMContextObserver { queue?.run { self.onNew?(rumContext) } } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index 8196086683..094f932c7f 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -4,9 +4,12 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation -import UIKit import XCTest +import UIKit +import WebKit + @_spi(Internal) @testable import DatadogSessionReplay @testable import TestUtilities @@ -34,7 +37,8 @@ extension ViewTreeSnapshot: AnyMockable, RandomMockable { context: .mockRandom(), viewportSize: .mockRandom(), nodes: .mockRandom(count: .random(in: (5..<50))), - resources: .mockRandom(count: .random(in: (5..<50))) + resources: .mockRandom(count: .random(in: (5..<50))), + webViewSlotIDs: .mockRandom() ) } @@ -43,14 +47,16 @@ extension ViewTreeSnapshot: AnyMockable, RandomMockable { context: Recorder.Context = .mockAny(), viewportSize: CGSize = .mockAny(), nodes: [Node] = .mockAny(), - resources: [Resource] = .mockAny() + resources: [Resource] = .mockAny(), + webViewSlotIDs: Set = .mockAny() ) -> ViewTreeSnapshot { return ViewTreeSnapshot( date: date, context: context, viewportSize: viewportSize, nodes: nodes, - resources: resources + resources: resources, + webViewSlotIDs: webViewSlotIDs ) } } @@ -297,7 +303,7 @@ extension UIImageResource: RandomMockable { } } -extension Collection where Element == Resource { +extension Array where Element == Resource { static func mockAny() -> [Resource] { return [MockResource].mockAny() } @@ -344,19 +350,22 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { return .init( recorder: .mockRandom(), coordinateSpace: UIView.mockRandom(), - ids: NodeIDGenerator() + ids: NodeIDGenerator(), + webViewCache: .weakObjects() ) } static func mockWith( recorder: Recorder.Context = .mockAny(), coordinateSpace: UICoordinateSpace = UIView.mockAny(), - ids: NodeIDGenerator = NodeIDGenerator() + ids: NodeIDGenerator = NodeIDGenerator(), + webViewCache: NSHashTable = .weakObjects() ) -> ViewTreeRecordingContext { return .init( recorder: recorder, coordinateSpace: coordinateSpace, - ids: ids + ids: ids, + webViewCache: webViewCache ) } } @@ -455,35 +464,6 @@ extension TouchSnapshot.Touch: AnyMockable, RandomMockable { // MARK: - Recorder Mocks -extension RUMContext: AnyMockable, RandomMockable { - public static func mockAny() -> RUMContext { - return .mockWith() - } - - public static func mockRandom() -> RUMContext { - return RUMContext( - applicationID: .mockRandom(), - sessionID: .mockRandom(), - viewID: .mockRandom(), - viewServerTimeOffset: .mockRandom() - ) - } - - static func mockWith( - applicationID: String = .mockAny(), - sessionID: String = .mockAny(), - viewID: String? = .mockAny(), - serverTimeOffset: TimeInterval = .mockAny() - ) -> RUMContext { - return RUMContext( - applicationID: applicationID, - sessionID: sessionID, - viewID: viewID, - viewServerTimeOffset: serverTimeOffset - ) - } -} - extension Recorder.Context: AnyMockable, RandomMockable { public static func mockAny() -> Recorder.Context { return .mockWith() @@ -601,3 +581,4 @@ internal extension Optional where Wrapped == NodeSemantics { return try XCTUnwrap(builders.first, file: file, line: line) } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift b/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift index b9a6ad3f2a..032dc4275b 100644 --- a/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation import TestUtilities @testable import DatadogSessionReplay @@ -51,3 +52,4 @@ extension EnrichedResource.Context: RandomMockable, AnyMockable { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift index ffe7659fba..0e8ea28f77 100644 --- a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @testable import DatadogSessionReplay @@ -15,3 +16,4 @@ internal class ResourceProcessorSpy: ResourceProcessing { processedResources.append((resources: resources, context: context)) } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift b/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift index 8565242b77..855bb9a3cf 100644 --- a/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @_spi(Internal) @testable import DatadogSessionReplay @@ -1262,3 +1263,4 @@ private extension Double { return Double(integer) } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift index 4d6f20e10b..a3e303ccc3 100644 --- a/DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @testable import DatadogSessionReplay @@ -16,3 +17,4 @@ internal class SnapshotProcessorSpy: SnapshotProcessing { processedSnapshots.append((viewTreeSnapshot, touchSnapshot)) } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/SnapshotProducerMocks.swift b/DatadogSessionReplay/Tests/Mocks/SnapshotProducerMocks.swift index 7eec519784..9562d9cc32 100644 --- a/DatadogSessionReplay/Tests/Mocks/SnapshotProducerMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/SnapshotProducerMocks.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @_spi(Internal) @testable import DatadogSessionReplay @@ -66,3 +67,4 @@ internal class TouchSnapshotProducerMock: TouchSnapshotProducer { return succeedingSnapshots.isEmpty ? nil : succeedingSnapshots.removeFirst() } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/TestScheduler.swift b/DatadogSessionReplay/Tests/Mocks/TestScheduler.swift index 344b3f09a9..3d17b99dbe 100644 --- a/DatadogSessionReplay/Tests/Mocks/TestScheduler.swift +++ b/DatadogSessionReplay/Tests/Mocks/TestScheduler.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation @testable import DatadogSessionReplay @@ -53,3 +54,4 @@ internal class TestScheduler: Scheduler { isRunning = false } } +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/UIKitMocks.swift b/DatadogSessionReplay/Tests/Mocks/UIKitMocks.swift index 7980192209..83e320b352 100644 --- a/DatadogSessionReplay/Tests/Mocks/UIKitMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/UIKitMocks.swift @@ -92,11 +92,11 @@ extension UITextContentType: RandomMockable { .username, .password ] - if #available(iOS 15.0, *) { + if #available(iOS 15.0, tvOS 15.0, *) { all.formUnion([.shipmentTrackingNumber, .flightNumber, .dateTime]) } - if #available(iOS 12.0, *) { + if #available(iOS 12.0, tvOS 12.0, *) { all.formUnion([.newPassword, .oneTimeCode]) } diff --git a/DatadogSessionReplay/Tests/Processor/SRDataModelsBuilder/RecordsBuilderTests.swift b/DatadogSessionReplay/Tests/Processor/Builders/RecordsBuilderTests.swift similarity index 99% rename from DatadogSessionReplay/Tests/Processor/SRDataModelsBuilder/RecordsBuilderTests.swift rename to DatadogSessionReplay/Tests/Processor/Builders/RecordsBuilderTests.swift index ec47cdede1..73a7287e69 100644 --- a/DatadogSessionReplay/Tests/Processor/SRDataModelsBuilder/RecordsBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Builders/RecordsBuilderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import TestUtilities @_spi(Internal) @@ -101,3 +102,4 @@ class RecordsBuilderTests: XCTestCase { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift new file mode 100644 index 0000000000..d39d85fb9d --- /dev/null +++ b/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift @@ -0,0 +1,64 @@ +/* + * 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. + */ + +#if os(iOS) +import XCTest +import TestUtilities + +@_spi(Internal) +@testable import DatadogSessionReplay + +class WireframesBuilderTests: XCTestCase { + func testBuildingVisibleWebVieWireframes_ItRemovesSlotFromCache() { + let slots: Set = .mockRandom(count: 10) + let builder = WireframesBuilder(webViewSlotIDs: slots) + + slots.forEach { id in + let frame: CGRect = .mockRandom() + let wireframe = builder.visibleWebViewWireframe(id: id, frame: frame) + guard case let .webviewWireframe(wireframe) = wireframe else { + return XCTFail("The wireframe must be webviewWireframe case") + } + + XCTAssertEqual(wireframe.id, Int64(id)) + XCTAssertEqual(wireframe.slotId, String(id)) + XCTAssertTrue(wireframe.isVisible ?? false) + XCTAssertNil(wireframe.border) + XCTAssertNil(wireframe.clip) + XCTAssertEqual(wireframe.height, Int64(withNoOverflow: frame.height)) + XCTAssertNil(wireframe.shapeStyle) + XCTAssertEqual(wireframe.width, Int64(withNoOverflow: frame.size.width)) + XCTAssertEqual(wireframe.x, Int64(withNoOverflow: frame.minX)) + XCTAssertEqual(wireframe.y, Int64(withNoOverflow: frame.minY)) + } + + XCTAssertTrue(builder.hiddenWebViewWireframes().isEmpty) + } + + func testBuildingHiddenWebVieWireframes_ItRemovesSlotFromCache() { + let slots: Set = .mockRandom(count: 10) + let builder = WireframesBuilder(webViewSlotIDs: slots) + + builder.hiddenWebViewWireframes().forEach { wireframe in + guard case let .webviewWireframe(wireframe) = wireframe else { + return XCTFail("The wireframe must be webviewWireframe case") + } + + XCTAssertEqual(String(wireframe.id), wireframe.slotId) + XCTAssertFalse(wireframe.isVisible ?? true) + XCTAssertNil(wireframe.border) + XCTAssertNil(wireframe.clip) + XCTAssertEqual(wireframe.height, 0) + XCTAssertNil(wireframe.shapeStyle) + XCTAssertEqual(wireframe.width, 0) + XCTAssertEqual(wireframe.x, 0) + XCTAssertEqual(wireframe.y, 0) + } + + XCTAssertTrue(builder.hiddenWebViewWireframes().isEmpty) + } +} +#endif diff --git a/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift b/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift index 402feb6e9c..28bd93a6c5 100644 --- a/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import TestUtilities @_spi(Internal) @@ -222,3 +223,4 @@ extension SRWireframe { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Processor/Diffing/DiffTests.swift b/DatadogSessionReplay/Tests/Processor/Diffing/DiffTests.swift index 795f2fb008..a6cebb7beb 100644 --- a/DatadogSessionReplay/Tests/Processor/Diffing/DiffTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Diffing/DiffTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @testable import TestUtilities @@ -380,3 +381,5 @@ extension Array where Element: Diffable { return result } } + +#endif diff --git a/DatadogSessionReplay/Tests/Processor/Flattening/NodesFlattenerTests.swift b/DatadogSessionReplay/Tests/Processor/Flattening/NodesFlattenerTests.swift index 1465352dff..5ef5bbbb25 100644 --- a/DatadogSessionReplay/Tests/Processor/Flattening/NodesFlattenerTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Flattening/NodesFlattenerTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -126,3 +127,4 @@ class NodesFlattenerTests: XCTestCase { DDAssertReflectionEqual(flattenedNodes, [intersectingNode]) } } +#endif diff --git a/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift b/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift index f0fd41cf4e..389071bbd7 100644 --- a/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @testable import TestUtilities @@ -70,3 +71,4 @@ class NOPTextObfuscatorTests: XCTestCase { XCTAssertEqual(obfuscator.mask(text: text), text) } } +#endif diff --git a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift index f53a334c96..7ed88fe5d6 100644 --- a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import DatadogInternal import TestUtilities @@ -95,3 +96,4 @@ class ResourceProcessorTests: XCTestCase { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift index 9e30dcb07b..a1b02ff2c8 100644 --- a/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift @@ -4,7 +4,9 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest +import WebKit import DatadogInternal import TestUtilities @@ -194,6 +196,91 @@ class SnapshotProcessorTests: XCTestCase { XCTAssertEqual(core.recordsCountByViewID?.values.map { $0 }, [4, 4]) } + func testWhenProcessingViewTreeSnapshot_itIncludeWebViewSlotFromNode() throws { + let time = Date() + let rum: RUMContext = .mockWith(serverTimeOffset: 0) + + // Given + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = SnapshotProcessor( + queue: NoQueue(), + recordWriter: recordWriter, + srContextPublisher: srContextPublisher, + telemetry: TelemetryMock() + ) + + let hiddenSlot: Int = .mockRandom() + let visibleSlot: Int = .mockRandom(otherThan: [hiddenSlot]) + let builder = WKWebViewWireframesBuilder(slotID: visibleSlot, attributes: .mockAny()) + let node = Node(viewAttributes: .mockAny(), wireframesBuilder: builder) + + // When + let snapshot = ViewTreeSnapshot( + date: time, + context: .init(privacy: .allow, rumContext: rum, date: time), + viewportSize: .mockRandom(minWidth: 1_000, minHeight: 1_000), + nodes: [node], + resources: [], + webViewSlotIDs: Set([hiddenSlot, visibleSlot]) + ) + + processor.process(viewTreeSnapshot: snapshot, touchSnapshot: nil) + + // Then + XCTAssertEqual(recordWriter.records.count, 1) + + let enrichedRecord = try XCTUnwrap(recordWriter.records.first) + XCTAssertEqual(enrichedRecord.applicationID, rum.applicationID) + XCTAssertEqual(enrichedRecord.sessionID, rum.sessionID) + XCTAssertEqual(enrichedRecord.viewID, rum.viewID) + + XCTAssertEqual(enrichedRecord.records.count, 3) + XCTAssertTrue(enrichedRecord.records[2].isFullSnapshotRecord) + let fullSnapshotRecord = try XCTUnwrap(enrichedRecord.records[2].fullSnapshot) + XCTAssertEqual(fullSnapshotRecord.data.wireframes.count, 2) + + XCTAssertEqual(fullSnapshotRecord.data.wireframes.first?.id, Int64(hiddenSlot), "The hidden webview wireframe should be first") + XCTAssertEqual(fullSnapshotRecord.data.wireframes.last?.id, Int64(visibleSlot), "The visible webview wireframe should be last") + } + + func testWhenProcessingViewTreeSnapshot_itIncludeWebViewSlotFromCache() throws { + let time = Date() + let rum: RUMContext = .mockWith(serverTimeOffset: 0) + + // Given + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = SnapshotProcessor( + queue: NoQueue(), + recordWriter: recordWriter, + srContextPublisher: srContextPublisher, + telemetry: TelemetryMock() + ) + + let webview = WKWebView() + let viewTree = generateSimpleViewTree() + + // When + snapshotBuilder.webViewCache.add(webview) + let snapshot = generateViewTreeSnapshot(for: viewTree, date: time, rumContext: rum) + processor.process(viewTreeSnapshot: snapshot, touchSnapshot: nil) + + // Then + XCTAssertEqual(recordWriter.records.count, 1) + + let enrichedRecord = try XCTUnwrap(recordWriter.records.first) + XCTAssertEqual(enrichedRecord.applicationID, rum.applicationID) + XCTAssertEqual(enrichedRecord.sessionID, rum.sessionID) + XCTAssertEqual(enrichedRecord.viewID, rum.viewID) + + XCTAssertEqual(enrichedRecord.records.count, 3) + XCTAssertTrue(enrichedRecord.records[2].isFullSnapshotRecord) + let fullSnapshotRecord = try XCTUnwrap(enrichedRecord.records[2].fullSnapshot) + XCTAssertEqual(fullSnapshotRecord.data.wireframes.count, 3) + XCTAssertEqual(fullSnapshotRecord.data.wireframes.first?.id, Int64(webview.hash), "The hidden webview wireframe should be first") + } + // MARK: - Processing `TouchSnapshots` func testWhenProcessingTouchSnapshot_itWritesRecordsThatContinueCurrentSegment() throws { @@ -346,3 +433,4 @@ fileprivate extension PassthroughCoreMock { return try? context.baggages["sr_records_count_by_view_id"]?.decode() } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/PrivacyLevelTests.swift b/DatadogSessionReplay/Tests/Recorder/PrivacyLevelTests.swift index 5a09bcc267..07315d2e66 100644 --- a/DatadogSessionReplay/Tests/Recorder/PrivacyLevelTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/PrivacyLevelTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -42,3 +43,4 @@ class PrivacyLevelTests: XCTestCase { XCTAssertTrue(PrivacyLevel.maskUserInput.shouldMaskInputElements) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift index 0a0205349e..8b690de9d3 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -137,3 +138,4 @@ class RecorderTests: XCTestCase { XCTAssertEqual(queryContext.recorder, recorderContext) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift b/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift index a6bb082b33..b6cb63abc5 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import DatadogInternal @_spi(Internal) @@ -136,3 +137,4 @@ final class RecordingMock: Recording { captureNextRecordClosure?(recorderContext) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGeneratorTests.swift b/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGeneratorTests.swift index 0f69518cc1..ae4037ddef 100644 --- a/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGeneratorTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGeneratorTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @testable import TestUtilities @@ -99,3 +100,4 @@ class TouchIdentifierGeneratorTests: XCTestCase { XCTAssertEqual(generator.touchIdentifier(for: UITouchMock(phase: .began)), 0) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift b/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift index 1a94140816..5c282bd317 100644 --- a/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -117,3 +118,4 @@ class WindowTouchSnapshotProducerTests: XCTestCase { XCTAssertGreaterThan(snapshot1!.date, Date()) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift index 10fc7f92ca..02d1c60c54 100644 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import UIKit @testable import DatadogSessionReplay @@ -191,3 +192,4 @@ class CGRectContentFrameTests: XCTestCase { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift deleted file mode 100644 index f7552d51bc..0000000000 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 -@_spi(Internal) -@testable import DatadogSessionReplay - -class ImageDataProviderTests: XCTestCase { - func test_returnsNil_WhenImageIsNil() { - let sut = ImageDataProvider() - - XCTAssertNil(sut.contentBase64String(of: nil)) - } - - func test_returnsEmptyStringAndIdentifier_WhenContentIsEmpty() { - let sut = ImageDataProvider() - let imageResource = sut.contentBase64String(of: UIImage()) - - XCTAssertEqual(imageResource?.base64, "") - XCTAssertEqual(imageResource?.identifier, "") - } - - func test_returnsValidStringAndIdentifier_WhenContentIsValid() throws { - if #available(iOS 17, *) { - throw XCTSkip("TODO: RUM-2969 Fix image hashing tests for iOS 17+") - } - let sut = ImageDataProvider() - let base64 = "R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" - let image = UIImage(data: Data(base64Encoded: base64)!) - - let imageResource = try XCTUnwrap(sut.contentBase64String(of: image)) - XCTAssertGreaterThan(imageResource.base64.count, 0) - XCTAssertEqual(imageResource.identifier, "258d046955bfb950883f0ed61b586b5a") - } - - func test_returnsValidStringAndIdentifier_forSFSymbolIcon() throws { - guard #available(iOS 13, *) else { - throw XCTSkip("UIImage(systemName:) is not available in this OS") - } - if #available(iOS 17, *) { - throw XCTSkip("TODO: RUM-2969 Fix image hashing tests for iOS 17+") - } - let sut = ImageDataProvider() - let image = UIImage(systemName: "square.and.arrow.up") - - let imageResource = try XCTUnwrap(sut.contentBase64String(of: image)) - XCTAssertGreaterThan(imageResource.base64.count, 0) - XCTAssertEqual(imageResource.identifier, "89187ac6043b08c088090d348fbb397f") - } - - func test_imageIdentifierConsistency() { - var ids = Set() - let image: UIImage = .mockRandom() - for _ in 0..<100 { - ids.insert(image.srIdentifier) - } - XCTAssertEqual(ids.count, 1) - } - - func test_colorIdentifierConsistency() { - var ids = Set() - for _ in 0..<100 { - ids.insert( UIColor.red.srIdentifier) - } - XCTAssertEqual(ids.count, 1) - } -} diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/UIKitExtensionsTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/UIKitExtensionsTests.swift index b3d927fedc..87c6bd891c 100644 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/UIKitExtensionsTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/Utilties/UIKitExtensionsTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import UIKit import TestUtilities @@ -62,3 +63,4 @@ class UIKitExtensionsTests: XCTestCase { } // swiftlint:enable opening_brace } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeIDGeneratorTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeIDGeneratorTests.swift index fc37d10ad3..85020f18f5 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeIDGeneratorTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeIDGeneratorTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import UIKit @_spi(Internal) @@ -105,3 +106,4 @@ class NodeIDGeneratorTests: XCTestCase { XCTAssertNotEqual(id, newID) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift index ec39138502..61eae4a363 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -52,3 +53,4 @@ class UIDatePickerRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageResourceTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageResourceTests.swift index 419d0dc650..5ed3b5ac32 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageResourceTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageResourceTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -61,3 +62,4 @@ final class UIImageResourceTests: XCTestCase { return image } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift index ac9166b1f6..e349169bf4 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -91,3 +92,4 @@ class UIImageViewRecorderTests: XCTestCase { } } // swiftlint:enable opening_brace +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift index b0a016fa9f..70e6de3ea6 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -78,3 +79,4 @@ class UIImageViewWireframesBuilderTests: XCTestCase { } } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift index eca8bd4ad0..ca24da4bbf 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import TestUtilities @_spi(Internal) @@ -86,3 +87,4 @@ class UILabelRecorderTests: XCTestCase { } } // swiftlint:enable opening_brace +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift index f177c674bc..1f7ae9b564 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -31,3 +32,4 @@ class UINavigationBarRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: .mockAny(), in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorderTests.swift index 521e5f5ccd..f55fd5a8cc 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -52,3 +53,4 @@ class UIPickerViewRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorderTests.swift index b5c248538a..f016bf2a26 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -54,3 +55,4 @@ class UISegmentRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorderTests.swift index b68ed142af..567a7fde3e 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -52,3 +53,4 @@ class UISliderRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift index 785eea0b45..c24cc0cebc 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -46,3 +47,4 @@ class UIStepperRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorderTests.swift index 153d30a261..666d0bde35 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -55,3 +56,4 @@ class UISwitchRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift index c0ef84b6e5..3f0b84f277 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -31,3 +32,4 @@ class UITabBarRecorderTests: XCTestCase { XCTAssertNil(recorder.semantics(of: view, with: .mockAny(), in: .mockAny())) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift index cb4f6ac5b6..948c017048 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import TestUtilities @_spi(Internal) @@ -105,3 +106,4 @@ class UITextFieldRecorderTests: XCTestCase { } } // swiftlint:enable opening_brace +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift index 84a024f7d5..5030b7feed 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -96,3 +97,4 @@ class UITextViewRecorderTests: XCTestCase { } } // swiftlint:enable opening_brace +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorderTests.swift index 1937ed9486..151851f188 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -57,3 +58,4 @@ class UIViewRecorderTests: XCTestCase { XCTAssertEqual(semantics.subtreeStrategy, .record) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorderTests.swift index a6a90b6d00..841147c827 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorderTests.swift @@ -5,6 +5,7 @@ */ import XCTest +#if os(iOS) import WebKit import SwiftUI import SafariServices @@ -16,12 +17,12 @@ class UnsupportedViewRecorderTests: XCTestCase { private let recorder = UnsupportedViewRecorder() private let unsupportedViews: [UIView] = [ - UIProgressView(), UIActivityIndicatorView(), WKWebView() + UIProgressView(), UIActivityIndicatorView() ].compactMap { $0 } private let expectedUnsupportedViewsClassNames = [ "UIProgressView", "UIActivityIndicatorView", "WKWebView" ] - private let otherViews = [UILabel(), UIView(), UIImageView(), UIScrollView()] + private let otherViews = [UILabel(), UIView(), UIImageView(), UIScrollView(), WKWebView()] /// `ViewAttributes` simulating common attributes of the view. private var viewAttributes: ViewAttributes = .mockAny() @@ -69,3 +70,5 @@ class UnsupportedViewRecorderTests: XCTestCase { XCTAssertNotNil(wireframeBuilder.unsupportedClassName) } } + +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorderTests.swift index f03be46989..92c87c893d 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorderTests.swift @@ -5,6 +5,7 @@ */ import XCTest +#if os(iOS) import WebKit import TestUtilities @@ -31,10 +32,14 @@ class WKWebViewRecorderTests: XCTestCase { let viewAttributes: ViewAttributes = .mock(fixture: .invisible) // When - let semantics = try XCTUnwrap(recorder.semantics(of: webView, with: viewAttributes, in: .mockAny())) + let semantics = try XCTUnwrap(recorder.semantics(of: webView, with: viewAttributes, in: .mockAny()) as? SpecificElement) // Then - XCTAssertTrue(semantics is InvisibleElement) + XCTAssertEqual(semantics.subtreeStrategy, .ignore, "WebView's subtree should not be recorded") + + let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? WKWebViewWireframesBuilder) + let wireframes = builder.buildWireframes(with: WireframesBuilder()) + XCTAssert(wireframes.isEmpty) } func testWhenWebViewIsVisible() throws { @@ -48,23 +53,20 @@ class WKWebViewRecorderTests: XCTestCase { XCTAssertEqual(semantics.subtreeStrategy, .ignore, "WebView's subtree should not be recorded") let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? WKWebViewWireframesBuilder) - XCTAssertEqual(builder.attributes, viewAttributes) + let wireframes = builder.buildWireframes(with: WireframesBuilder()) + XCTAssertFalse(wireframes.isEmpty) } - func testWebViewWireframeBuilder() throws { + func testVisibleWebViewSlot() throws { // Given - let id: WireframeID = .mockRandom() - let slotId: Int = .mockRandom() let attributes: ViewAttributes = .mock(fixture: .visible()) + let slotID = webView.hash - let builder = WKWebViewWireframesBuilder( - wireframeID: id, - slotID: slotId, - attributes: attributes - ) + let builder = WireframesBuilder(webViewSlotIDs: [slotID]) // When - let wireframes = builder.buildWireframes(with: WireframesBuilder()) + let wireframes = WKWebViewWireframesBuilder(slotID: webView.hash, attributes: attributes) + .buildWireframes(with: builder) // Then XCTAssertEqual(wireframes.count, 1) @@ -73,12 +75,16 @@ class WKWebViewRecorderTests: XCTestCase { return XCTFail("First wireframe needs to be webviewWireframe case") } - XCTAssertEqual(wireframe.id, id) - XCTAssertEqual(wireframe.slotId, String(slotId)) + XCTAssertEqual(wireframe.id, Int64(webView.hash)) + XCTAssertEqual(wireframe.slotId, String(webView.hash)) XCTAssertNil(wireframe.clip) XCTAssertEqual(wireframe.x, Int64(withNoOverflow: attributes.frame.minX)) XCTAssertEqual(wireframe.y, Int64(withNoOverflow: attributes.frame.minY)) XCTAssertEqual(wireframe.width, Int64(withNoOverflow: attributes.frame.width)) XCTAssertEqual(wireframe.height, Int64(withNoOverflow: attributes.frame.height)) + XCTAssertTrue(wireframe.isVisible ?? false) + XCTAssertTrue(builder.hiddenWebViewWireframes().isEmpty, "webview slot should be removed from builder") } } + +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index 380d2ceb64..6febca459c 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import SafariServices @@ -250,24 +251,39 @@ class ViewTreeRecorderTests: XCTestCase { } func testItOverridesViewControllerContext() throws { - if #available(iOS 17, *) { - throw XCTSkip("TODO: RUM-3134 Fix `ViewTreeRecorderTests` on iOS 17.0+") - } let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) let recorder = ViewTreeRecorder(nodeRecorders: [nodeRecorder]) - let views = [ - UIAlertController(title: "", message: "", preferredStyle: .alert).view, - UIView(), - UIViewController().view, - UIView(), - SFSafariViewController(url: .mockRandom()).view, - UIView(), - UIActivityViewController(activityItems: [], applicationActivities: nil).view, - UIView() - ].compactMap { $0 } - - zip(views, views.dropFirst()).forEach { - $0.0.addSubview($0.1) + + // swiftlint:disable opening_brace + let nodes: [(UIWindow) -> UIView] = [ + { window in + let vc = UIViewController() + window.rootViewController = vc + return vc.view + }, + { _ in UIView() }, + { _ in UIAlertController(title: "", message: "", preferredStyle: .alert).view }, + { _ in UIView() }, + { window in + let safari = SFSafariViewController(url: .mockRandom()) + window.rootViewController = safari + return safari.view + }, + { _ in UIView() }, + { _ in UIActivityViewController(activityItems: [], applicationActivities: nil).view }, + { _ in UIView() } + ] + // swiftlint:enable opening_brace + + // Build the tree in the window + let window = UIWindow() + var parent: UIView? = nil + + let views = nodes.map { node in + let view = node(window) + parent?.addSubview(view) + parent = view + return view } // When @@ -276,19 +292,19 @@ class ViewTreeRecorderTests: XCTestCase { // Then var context = nodeRecorder.queryContextsByView[views[0]] XCTAssertEqual(context?.viewControllerContext.isRootView, true) - XCTAssertEqual(context?.viewControllerContext.parentType, .alert) + XCTAssertEqual(context?.viewControllerContext.parentType, .other) context = nodeRecorder.queryContextsByView[views[1]] XCTAssertEqual(context?.viewControllerContext.isRootView, false) - XCTAssertEqual(context?.viewControllerContext.parentType, .alert) + XCTAssertEqual(context?.viewControllerContext.parentType, .other) context = nodeRecorder.queryContextsByView[views[2]] XCTAssertEqual(context?.viewControllerContext.isRootView, true) - XCTAssertEqual(context?.viewControllerContext.parentType, .other) + XCTAssertEqual(context?.viewControllerContext.parentType, .alert) context = nodeRecorder.queryContextsByView[views[3]] XCTAssertEqual(context?.viewControllerContext.isRootView, false) - XCTAssertEqual(context?.viewControllerContext.parentType, .other) + XCTAssertEqual(context?.viewControllerContext.parentType, .alert) context = nodeRecorder.queryContextsByView[views[4]] XCTAssertEqual(context?.viewControllerContext.isRootView, true) @@ -307,3 +323,4 @@ class ViewTreeRecorderTests: XCTestCase { XCTAssertEqual(context?.viewControllerContext.parentType, .activity) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContextTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContextTests.swift index ca4cb28b7a..b06dea02fe 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContextTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContextTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import SafariServices @_spi(Internal) @@ -58,3 +59,4 @@ class ViewTreeRecordingContextTests: XCTestCase { XCTAssertNil(viewControllerContext.name) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift index 2e1842ab34..ccb365c7a3 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -66,3 +67,4 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { XCTAssertEqual(queryContext.recorder, randomRecorderContext) } } +#endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift index a3c78cee5e..cb586be43b 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -217,3 +218,4 @@ class NodeSemanticsTests: XCTestCase { ) } } +#endif diff --git a/DatadogSessionReplay/Tests/SessionReplayConfigurationTests.swift b/DatadogSessionReplay/Tests/SessionReplayConfigurationTests.swift index 74d4eaf65d..44c8439d0c 100644 --- a/DatadogSessionReplay/Tests/SessionReplayConfigurationTests.swift +++ b/DatadogSessionReplay/Tests/SessionReplayConfigurationTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @_spi(Internal) @testable import DatadogSessionReplay @@ -36,3 +37,4 @@ class SessionReplayConfigurationTests: XCTestCase { XCTAssertEqual(config._additionalNodeRecorders[0].identifier, mockNodeRecorder.identifier) } } +#endif diff --git a/DatadogSessionReplay/Tests/SessionReplayTests.swift b/DatadogSessionReplay/Tests/SessionReplayTests.swift index c3aa6eed6b..c724a8e392 100644 --- a/DatadogSessionReplay/Tests/SessionReplayTests.swift +++ b/DatadogSessionReplay/Tests/SessionReplayTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import TestUtilities @testable import DatadogInternal @@ -164,3 +165,4 @@ class SessionReplayTests: XCTestCase { XCTAssertNil(core.get(feature: ResourcesFeature.self)) } } +#endif diff --git a/DatadogSessionReplay/Tests/Utilities/CFType+SafetyTests.swift b/DatadogSessionReplay/Tests/Utilities/CFType+SafetyTests.swift index 20c632788a..d2b5cbec59 100644 --- a/DatadogSessionReplay/Tests/Utilities/CFType+SafetyTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/CFType+SafetyTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation import XCTest @testable import DatadogSessionReplay @@ -18,3 +19,4 @@ class CFTypeSafetyTests: XCTestCase { XCTAssertNil(invalid.safeCast) } } +#endif diff --git a/DatadogSessionReplay/Tests/Utilities/CGRectExtensionsTests.swift b/DatadogSessionReplay/Tests/Utilities/CGRectExtensionsTests.swift index 58b214f4e5..f40b5386f1 100644 --- a/DatadogSessionReplay/Tests/Utilities/CGRectExtensionsTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/CGRectExtensionsTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -40,3 +41,4 @@ class CGRectExtensionsTests: XCTestCase { XCTAssertEqual(insideCM, .init(x: 40, y: 95, width: 40, height: 50)) } } +#endif diff --git a/DatadogSessionReplay/Tests/Utilities/ColorsTests.swift b/DatadogSessionReplay/Tests/Utilities/ColorsTests.swift index 7b72ab3a18..f9ed112161 100644 --- a/DatadogSessionReplay/Tests/Utilities/ColorsTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/ColorsTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @testable import TestUtilities @@ -79,3 +80,4 @@ class ColorsTests: XCTestCase { XCTAssertNil(hexString(from: color)) } } +#endif diff --git a/DatadogSessionReplay/Tests/Utilities/QueueTests.swift b/DatadogSessionReplay/Tests/Utilities/QueueTests.swift index d348bc2c48..dd38598b3d 100644 --- a/DatadogSessionReplay/Tests/Utilities/QueueTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/QueueTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @testable import TestUtilities @@ -51,3 +52,4 @@ class QueueTests: XCTestCase { XCTAssertNotEqual(value, randomValue) } } +#endif diff --git a/DatadogSessionReplay/Tests/Utilities/Schedulers/MainThreadSchedulerTests.swift b/DatadogSessionReplay/Tests/Utilities/Schedulers/MainThreadSchedulerTests.swift index 79dbd9c497..eb826f5f5f 100644 --- a/DatadogSessionReplay/Tests/Utilities/Schedulers/MainThreadSchedulerTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/Schedulers/MainThreadSchedulerTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -115,3 +116,4 @@ class MainThreadSchedulerTests: XCTestCase { XCTAssertGreaterThanOrEqual(belowIntervalCount, p25, "At least 25% of repeats should be below \(interval)s") } } +#endif diff --git a/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift b/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift index d5ef20fa53..69f875d795 100644 --- a/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import Foundation import XCTest @testable import DatadogSessionReplay @@ -29,3 +30,4 @@ class UIImageScalingTests: XCTestCase { XCTAssertTrue(scaledData.count < dataSize) } } +#endif diff --git a/DatadogSessionReplay/Tests/Writer/RecordsWriterTests.swift b/DatadogSessionReplay/Tests/Writer/RecordsWriterTests.swift index 9f0a249c18..5bb6192c67 100644 --- a/DatadogSessionReplay/Tests/Writer/RecordsWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/RecordsWriterTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest @testable import DatadogSessionReplay @@ -42,3 +43,4 @@ class RecordsWriterTests: XCTestCase { } } // swiftlint:enable empty_xctest_method +#endif diff --git a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift index 0250bab4b0..23335312f4 100644 --- a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import XCTest import DatadogInternal @@ -128,3 +129,4 @@ class ResourcesWriterTests: XCTestCase { XCTAssertEqual(scopeMock.telemetryMock.messages[0].asError?.message, "Failed to decode store creation - invalidData") } } +#endif diff --git a/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift b/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift index bd355913bc..c82494b771 100644 --- a/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift +++ b/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift @@ -4,6 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ +#if os(iOS) import zlib import XCTest import Compression @@ -73,3 +74,4 @@ class SRCompressionTests: XCTestCase { XCTAssertEqual(compressed5, expected5) } } +#endif diff --git a/DatadogTrace/Sources/DDFormat.swift b/DatadogTrace/Sources/DDFormat.swift index 01d52cb045..24c96b87a4 100644 --- a/DatadogTrace/Sources/DDFormat.swift +++ b/DatadogTrace/Sources/DDFormat.swift @@ -20,11 +20,14 @@ extension TracePropagationHeadersWriter where Self: OTFormatWriter { guard let spanContext = spanContext.dd else { return } - write( - traceID: spanContext.traceID, - spanID: spanContext.spanID, - parentSpanID: spanContext.parentSpanID + traceContext: TraceContext( + traceID: spanContext.traceID, + spanID: spanContext.spanID, + parentSpanID: spanContext.parentSpanID, + sampleRate: spanContext.sampleRate, + isKept: spanContext.isKept + ) ) } } @@ -38,7 +41,13 @@ extension TracePropagationHeadersReader where Self: OTFormatReader { traceID: ids.traceID, spanID: ids.spanID, parentSpanID: ids.parentSpanID, - baggageItems: BaggageItems() + baggageItems: BaggageItems(), + // RUM-3470: The `0` sample rate set here is only a placeholder value. It is overwritten with + // the actual value in the caller: `Tracer.extract(reader)`. + sampleRate: 0, + // RUM-3470: The `false` default will be never reached. As we got trace and span ID, + // it means that the request has been instrumented, so sampling decision was read as well. + isKept: sampled ?? false ) } } diff --git a/DatadogTrace/Sources/DDSpan.swift b/DatadogTrace/Sources/DDSpan.swift index d1c18fcab6..b25dbb6779 100644 --- a/DatadogTrace/Sources/DDSpan.swift +++ b/DatadogTrace/Sources/DDSpan.swift @@ -137,8 +137,8 @@ internal final class DDSpan: OTSpan { operationName: self.operationName, startTime: self.startTime, finishTime: finishTime, - samplingRate: sampler.samplingRate / 100.0, - isKept: sampler.sample(), + samplingRate: self.ddContext.sampleRate / 100.0, + isKept: self.ddContext.isKept, tags: self.tags, baggageItems: self.ddContext.baggageItems.all, logFields: self.logFields diff --git a/DatadogTrace/Sources/DDSpanContext.swift b/DatadogTrace/Sources/DDSpanContext.swift index 825403c0e6..47bac5d818 100644 --- a/DatadogTrace/Sources/DDSpanContext.swift +++ b/DatadogTrace/Sources/DDSpanContext.swift @@ -16,6 +16,12 @@ internal struct DDSpanContext: OTSpanContext { let parentSpanID: SpanID? /// The baggage items of this span. let baggageItems: BaggageItems + /// The sample rate used for sampling this span. + /// + /// It is a value between `0.0` (drop) and `100.0` (keep), determined by the local or distributed trace sampler. + let sampleRate: Float + /// Whether this span was sampled or rejected by the sampler. + let isKept: Bool // MARK: - Open Tracing interface diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index 6f59610d6e..eb77ea4af7 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -79,7 +79,7 @@ internal class DatadogTracer: OTTracer { func startSpan(operationName: String, references: [OTReference]? = nil, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { let parentSpanContext = references?.compactMap { $0.context.dd }.last ?? activeSpan?.context as? DDSpanContext return startSpan( - spanContext: createSpanContext(parentSpanContext: parentSpanContext), + spanContext: createSpanContext(parentSpanContext: parentSpanContext, using: localTraceSampler), operationName: operationName, tags: tags, startTime: startTime @@ -88,7 +88,7 @@ internal class DatadogTracer: OTTracer { func startRootSpan(operationName: String, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { return startSpan( - spanContext: createSpanContext(parentSpanContext: nil), + spanContext: createSpanContext(parentSpanContext: nil, using: localTraceSampler), operationName: operationName, tags: tags, startTime: startTime @@ -101,7 +101,18 @@ internal class DatadogTracer: OTTracer { func extract(reader: OTFormatReader) -> OTSpanContext? { // TODO: RUMM-385 - make `HTTPHeadersReader` available in public API - reader.extract() + guard let context = reader.extract() as? DDSpanContext else { + return nil + } + + return DDSpanContext( + traceID: context.traceID, + spanID: context.spanID, + parentSpanID: context.parentSpanID, + baggageItems: context.baggageItems, + sampleRate: localTraceSampler.samplingRate, + isKept: context.isKept + ) } var activeSpan: OTSpan? { @@ -110,12 +121,14 @@ internal class DatadogTracer: OTTracer { // MARK: - Internal - internal func createSpanContext(parentSpanContext: DDSpanContext? = nil) -> DDSpanContext { + internal func createSpanContext(parentSpanContext: DDSpanContext?, using sampler: Sampler) -> DDSpanContext { return DDSpanContext( traceID: parentSpanContext?.traceID ?? traceIDGenerator.generate(), spanID: spanIDGenerator.generate(), parentSpanID: parentSpanContext?.spanID, - baggageItems: BaggageItems(parent: parentSpanContext?.baggageItems) + baggageItems: BaggageItems(parent: parentSpanContext?.baggageItems), + sampleRate: parentSpanContext?.sampleRate ?? sampler.samplingRate, + isKept: parentSpanContext?.isKept ?? sampler.sample() ) } diff --git a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift index 2645277273..9dec872ec5 100644 --- a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift +++ b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift @@ -29,63 +29,62 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { self.firstPartyHosts = firstPartyHosts } - func modify(request: URLRequest, headerTypes: Set) -> URLRequest { + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { guard let tracer = tracer else { - return request + return (request, nil) } // Use the current active span as parent if the propagation // headers support it. let parentSpanContext = tracer.activeSpan?.context as? DDSpanContext - let spanContext = tracer.createSpanContext(parentSpanContext: parentSpanContext) + let spanContext = tracer.createSpanContext( + parentSpanContext: parentSpanContext, + using: distributedTraceSampler + ) + let injectedSpanContext = TraceContext( + traceID: spanContext.traceID, + spanID: spanContext.spanID, + parentSpanID: spanContext.parentSpanID, + sampleRate: spanContext.sampleRate, + isKept: spanContext.isKept + ) var request = request + var hasSetAnyHeader = false headerTypes.forEach { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(sampler: distributedTraceSampler) + writer = HTTPHeadersWriter(samplingStrategy: .headBased) case .b3: writer = B3HTTPHeadersWriter( - sampler: distributedTraceSampler, + samplingStrategy: .headBased, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - sampler: distributedTraceSampler, + samplingStrategy: .headBased, injectEncoding: .multiple ) case .tracecontext: - writer = W3CHTTPHeadersWriter(sampler: distributedTraceSampler, tracestate: [:]) + writer = W3CHTTPHeadersWriter( + samplingStrategy: .headBased, + tracestate: [:] + ) } - writer.write( - traceID: spanContext.traceID, - spanID: spanContext.spanID, - parentSpanID: spanContext.parentSpanID - ) + writer.write(traceContext: injectedSpanContext) writer.traceHeaderFields.forEach { field, value in // do not overwrite existing header if request.value(forHTTPHeaderField: field) == nil { + hasSetAnyHeader = true request.setValue(value, forHTTPHeaderField: field) } } } - return request - } - - func traceContext() -> DatadogInternal.TraceContext? { - guard let context = tracer?.activeSpan?.context as? DDSpanContext else { - return nil - } - - return TraceContext( - traceID: context.traceID, - spanID: context.spanID, - parentSpanID: context.parentSpanID - ) + return (request, (hasSetAnyHeader && injectedSpanContext.isKept) ? injectedSpanContext : nil) } func interceptionDidStart(interception: DatadogInternal.URLSessionTaskInterception) { @@ -110,7 +109,9 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { traceID: trace.traceID, spanID: trace.spanID, parentSpanID: trace.parentSpanID, - baggageItems: .init() + baggageItems: .init(), + sampleRate: trace.sampleRate, + isKept: trace.isKept ) span = tracer.startSpan( diff --git a/DatadogTrace/Sources/TraceConfiguration.swift b/DatadogTrace/Sources/TraceConfiguration.swift index eb719200de..0de05d1825 100644 --- a/DatadogTrace/Sources/TraceConfiguration.swift +++ b/DatadogTrace/Sources/TraceConfiguration.swift @@ -17,6 +17,7 @@ import DatadogInternal @_exported import class DatadogInternal.HTTPHeadersWriter @_exported import class DatadogInternal.B3HTTPHeadersWriter @_exported import class DatadogInternal.W3CHTTPHeadersWriter +@_exported import enum DatadogInternal.TraceSamplingStrategy // swiftlint:enable duplicate_imports extension Trace { diff --git a/DatadogTrace/Tests/DDNoopTracerTests.swift b/DatadogTrace/Tests/DDNoopTracerTests.swift index 0b24fa9055..ecea81ad79 100644 --- a/DatadogTrace/Tests/DDNoopTracerTests.swift +++ b/DatadogTrace/Tests/DDNoopTracerTests.swift @@ -20,7 +20,7 @@ class DDNoopTracerTests: XCTestCase { // When let context = DDSpanContext.mockAny() - noop.inject(spanContext: context, writer: HTTPHeadersWriter()) + noop.inject(spanContext: context, writer: HTTPHeadersWriter(samplingStrategy: .headBased)) _ = noop.extract(reader: HTTPHeadersReader(httpHeaderFields: [:])) let root = noop.startRootSpan(operationName: "root operation").setActive() let child = noop.startSpan(operationName: "child operation") diff --git a/DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift b/DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift new file mode 100644 index 0000000000..fd644a6466 --- /dev/null +++ b/DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift @@ -0,0 +1,99 @@ +/* + * 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 +import TestUtilities +import DatadogInternal +@testable import DatadogTrace + +private class MockWriter: OTFormatWriter, TracePropagationHeadersWriter { + var traceHeaderFields: [String: String] = [:] + var injectedTraceContext: TraceContext? + func write(traceContext: TraceContext) { injectedTraceContext = traceContext } +} + +private class MockReader: OTFormatReader, TracePropagationHeadersReader { + var extractedIDs: (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? = nil + var extractedIsKept: Bool? = nil + + func read() -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { extractedIDs } + var sampled: Bool? { extractedIsKept } +} + +class DatadogTracer_InjectAndExtract: XCTestCase { + private func createTracer(sampleRate: Float) -> DatadogTracer { + return DatadogTracer( + featureScope: NOPFeatureScope(), + localTraceSampler: Sampler(samplingRate: sampleRate), + tags: [:], + traceIDGenerator: DefaultTraceIDGenerator(), + spanIDGenerator: DefaultSpanIDGenerator(), + dateProvider: DateProviderMock(), + loggingIntegration: .mockAny(), + spanEventBuilder: .mockAny() + ) + } + + func testInjectingSpanContextIntoWriter() { + // Given + let spanContext = DDSpanContext( + traceID: .mockRandom(), + spanID: .mockRandom(), + parentSpanID: .mockRandom(), + baggageItems: .mockAny(), + sampleRate: .mockRandom(min: 0, max: 100), + isKept: .random() + ) + + let tracer = createTracer(sampleRate: 42) + let writer = MockWriter() + XCTAssertNil(writer.injectedTraceContext) + + // When + tracer.inject(spanContext: spanContext, writer: writer) + + // Then + let expectedTraceContext = TraceContext( + traceID: spanContext.traceID, + spanID: spanContext.spanID, + parentSpanID: spanContext.parentSpanID, + sampleRate: spanContext.sampleRate, + isKept: spanContext.isKept + ) + XCTAssertEqual(writer.injectedTraceContext, expectedTraceContext) + } + + func testExtractnigSpanContextFromReader() throws { + // Given + let tracer = createTracer(sampleRate: 42) + let reader = MockReader() + let ids: (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) = (.mockRandom(), .mockRandom(), .mockRandom()) + let isKept: Bool = .mockRandom() + reader.extractedIDs = ids + reader.extractedIsKept = isKept + + // When + let spanContext = try XCTUnwrap(tracer.extract(reader: reader) as? DDSpanContext) + + // Then + XCTAssertEqual(spanContext.traceID, ids.traceID) + XCTAssertEqual(spanContext.spanID, ids.spanID) + XCTAssertEqual(spanContext.parentSpanID, ids.parentSpanID) + XCTAssertEqual(spanContext.sampleRate, 42) + XCTAssertEqual(spanContext.isKept, isKept) + } + + func testExtractsEmptySpanContextFromReader() throws { + // Given + let tracer = createTracer(sampleRate: 42) + let reader = MockReader() + reader.extractedIDs = nil + reader.extractedIsKept = nil + + // When + XCTAssertNil(tracer.extract(reader: reader)) + } +} diff --git a/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift b/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift index e815d26dd8..55955fafb2 100644 --- a/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift +++ b/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift @@ -81,7 +81,6 @@ class DatadogTracer_SamplingTests: XCTestCase { XCTAssertEqual(events.filter({ $0.samplingRate == 0.42 }).count, 3, "All spans must encode the same sample rate") } - // TODO: RUM-3470 Enable this test when head-based sampling is supported func testWhenRootSpanIsSampled_thenAllChildSpansMustBeSampledTheSameWay() throws { // When let tracer = createTracer(sampleRate: 50) diff --git a/DatadogTrace/Tests/TracingFeatureMocks.swift b/DatadogTrace/Tests/TracingFeatureMocks.swift index 6ffdae242c..59f4bb5b84 100644 --- a/DatadogTrace/Tests/TracingFeatureMocks.swift +++ b/DatadogTrace/Tests/TracingFeatureMocks.swift @@ -21,13 +21,17 @@ extension DDSpanContext { traceID: TraceID = .mockAny(), spanID: SpanID = .mockAny(), parentSpanID: SpanID? = .mockAny(), - baggageItems: BaggageItems = .mockAny() + baggageItems: BaggageItems = .mockAny(), + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() ) -> DDSpanContext { return DDSpanContext( traceID: traceID, spanID: spanID, parentSpanID: parentSpanID, - baggageItems: baggageItems + baggageItems: baggageItems, + sampleRate: sampleRate, + isKept: isKept ) } } diff --git a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift index dd791ab814..e52be47ae0 100644 --- a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift +++ b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift @@ -53,7 +53,7 @@ class TracingURLSessionHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [ .datadog, @@ -63,9 +63,9 @@ class TracingURLSessionHandlerTests: XCTestCase { ] ) - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "64") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "100") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "_dd.p.tid=a") - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "64") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "100") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "000000000000000a0000000000000064") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField), "0000000000000064") @@ -73,6 +73,14 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "1") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000064-1") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-01") + XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "dd=p:0000000000000064;s:1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itDoesNotOverwriteTraceHeaders() throws { @@ -85,19 +93,21 @@ class TracingURLSessionHandlerTests: XCTestCase { ) // When - var request: URLRequest = .mockWith(url: "https://www.example.com") - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) - request.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) - - request = handler.modify( - request: request, + var orgRequest: URLRequest = .mockWith(url: "https://www.example.com") + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.tagsField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.tracestate) + + let (request, traceContext) = handler.modify( + request: orgRequest, headerTypes: [ .datadog, .b3, @@ -107,6 +117,7 @@ class TracingURLSessionHandlerTests: XCTestCase { ) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "custom") @@ -115,6 +126,9 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "custom") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectTraceHeaders() throws { @@ -127,7 +141,7 @@ class TracingURLSessionHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [ .datadog, @@ -146,6 +160,8 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "0") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "0") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-00") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withActiveSpan_itInjectParentSpanID() throws { @@ -161,7 +177,7 @@ class TracingURLSessionHandlerTests: XCTestCase { span.setActive() // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [ .datadog, @@ -173,9 +189,9 @@ class TracingURLSessionHandlerTests: XCTestCase { span.finish() - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "64") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "100") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "_dd.p.tid=a") - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "65") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "101") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "000000000000000a0000000000000064") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField), "0000000000000065") @@ -183,10 +199,19 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "1") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000065-1-0000000000000064") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000065-01") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 101) + XCTAssertEqual(injectedTraceContext.parentSpanID, span.context.dd.spanID) + XCTAssertEqual(injectedTraceContext.sampleRate, span.context.dd.sampleRate) + XCTAssertEqual(injectedTraceContext.isKept, span.context.dd.isKept) } func testGivenFirstPartyInterceptionWithSpanContext_whenInterceptionCompletes_itUsesInjectedSpanContext() throws { core.expectation = expectation(description: "Send span") + let sampleRate: Float = .mockRandom(min: 1, max: 100) + let isKept: Bool = .mockRandom() // Given let interception = URLSessionTaskInterception( @@ -205,7 +230,9 @@ class TracingURLSessionHandlerTests: XCTestCase { interception.register(trace: TraceContext( traceID: 100, spanID: 200, - parentSpanID: nil + parentSpanID: nil, + sampleRate: sampleRate, + isKept: isKept )) // When @@ -222,6 +249,8 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.operationName, "urlsession.request") XCTAssertFalse(span.isError) XCTAssertEqual(span.duration, 1) + XCTAssertEqual(span.samplingRate, sampleRate / 100) + XCTAssertEqual(span.isKept, isKept) } func testGivenFirstPartyInterceptionWithNoError_whenInterceptionCompletes_itEncodesRequestInfoInSpan() throws { @@ -258,70 +287,6 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.tags.count, 5) } - func testTraceContext_whenInterceptionStarts_withActiveSpan_itReturnCurrentSpan() { - // When - let span = tracer.startRootSpan(operationName: "root") - span.setActive() - // Then - let context = handler.traceContext() - XCTAssertEqual(context?.traceID, TraceID(idHi: 10, idLo: 100)) - XCTAssertEqual(context?.spanID, SpanID(100)) - - // When - span.finish() - // Then - XCTAssertNil(handler.traceContext()) - } - - func testGivenFirstPartyInterception_whenInterceptionStarts_withActiveSpan_itSendParentSpanID() throws { - core.expectation = expectation(description: "Send span") - core.expectation?.expectedFulfillmentCount = 2 - - // Given - let request: ImmutableRequest = .mockWith(httpMethod: "POST") - let interception = URLSessionTaskInterception(request: request, isFirstParty: true) - - // When - let span = tracer.startRootSpan(operationName: "root") - span.setActive() - interception.register(trace: TraceContext( - traceID: span.context.dd.traceID, - spanID: SpanID(300), - parentSpanID: span.context.dd.spanID - )) - handler.interceptionDidStart(interception: interception) - // Then - XCTAssertEqual(interception.trace?.parentSpanID?.rawValue, 100) - - // When - span.finish() - interception.register(response: .mockResponseWith(statusCode: 200), error: nil) - interception.register( - metrics: .mockWith( - fetch: .init( - start: .mockDecember15th2019At10AMUTC(), - end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 2) - ) - ) - ) - handler.interceptionDidComplete(interception: interception) - - // Then - waitForExpectations(timeout: 0.5, handler: nil) - - let envelopes: [SpanEventsEnvelope] = core.events() - let event1 = try XCTUnwrap(envelopes.first?.spans.first) - XCTAssertEqual(event1.operationName, "root") - XCTAssertEqual(event1.traceID, TraceID(idHi: 10, idLo: 100)) - XCTAssertEqual(event1.spanID, SpanID(100)) - XCTAssertNil(event1.parentID) - let event2 = try XCTUnwrap(envelopes.last?.spans.first) - XCTAssertEqual(event2.operationName, "urlsession.request") - XCTAssertEqual(event2.traceID, TraceID(idHi: 10, idLo: 100)) - XCTAssertEqual(event2.parentID, SpanID(100)) - XCTAssertEqual(event2.spanID, SpanID(300)) - } - func testGivenFirstPartyIncompleteInterception_whenInterceptionCompletes_itDoesNotSendTheSpan() throws { core.expectation = expectation(description: "Do not send span") core.expectation?.isInverted = true diff --git a/DatadogWebViewTracking/Sources/DDScriptMessageHandler.swift b/DatadogWebViewTracking/Sources/DDScriptMessageHandler.swift index aaba9df396..cebbedee73 100644 --- a/DatadogWebViewTracking/Sources/DDScriptMessageHandler.swift +++ b/DatadogWebViewTracking/Sources/DDScriptMessageHandler.swift @@ -28,11 +28,11 @@ internal class DDScriptMessageHandler: NSObject, WKScriptMessageHandler { _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { - let hash = userContentController.hash + let hash = message.webView.map { String($0.hash) } // message.body must be called within UI thread let body = message.body queue.async { - self.emitter.send(body: body, slotId: String(hash)) + self.emitter.send(body: body, slotId: hash) } } } diff --git a/DatadogWebViewTracking/Sources/WebViewTracking.swift b/DatadogWebViewTracking/Sources/WebViewTracking.swift index 167c04156c..94751a4d5c 100644 --- a/DatadogWebViewTracking/Sources/WebViewTracking.swift +++ b/DatadogWebViewTracking/Sources/WebViewTracking.swift @@ -25,8 +25,38 @@ import WebKit /// - Support users that have difficulty loading web pages on mobile devices public enum WebViewTracking { #if !os(tvOS) - /// Enables SDK to correlate Datadog RUM events and Logs from the WebView with native RUM session. + /// The Session Replay configuration to capture records coming from the web view. /// + /// Setting the Session Replay configuration in `WebViewTracking` will enable transmitting replay data from + /// the Datadog Browser SDK installed in the web page. Datadog will then be able to combine the native + /// and web recordings in a single replay. + public struct SessionReplayConfiguration { + /// Available privacy levels for content masking. + public enum PrivacyLevel: String { + /// Record all content. + case allow + + /// Mask all content. + case mask + + /// Mask input elements, but record all other content. + case maskUserInput = "mask_user_input" + } + + /// The privacy level to use for the web view replay recording. + public var privacyLevel: PrivacyLevel + + /// Creates Webview Session Replay configuration. + /// + /// - Parameters: + /// - privacyLevel: The way sensitive content (e.g. text) should be masked. Default: `.mask`. + public init(privacyLevel: PrivacyLevel = .mask) { + self.privacyLevel = privacyLevel + } + } + + /// Enables SDK to correlate Datadog RUM events and Logs from the WebView with native RUM session. + /// /// If the content loaded in WebView uses Datadog Browser SDK (`v4.2.0+`) and matches specified /// `hosts`, web events will be correlated with the RUM session from native SDK. /// @@ -35,11 +65,13 @@ public enum WebViewTracking { /// - hosts: A set of hosts instrumented with Browser SDK to capture Datadog events from. /// - logsSampleRate: The sampling rate for logs coming from the WebView. Must be a value between `0` and `100`, /// where 0 means no logs will be sent and 100 means all will be uploaded. Default: `100`. + /// - sessionReplayConfiguration: Session Replay configuration to enable linking Web and Native replays. /// - core: Datadog SDK core to use for tracking. public static func enable( webView: WKWebView, hosts: Set = [], logsSampleRate: Float = 100, + sessionReplayConfiguration: SessionReplayConfiguration? = nil, in core: DatadogCoreProtocol = CoreRegistry.default ) { enable( @@ -47,6 +79,7 @@ public enum WebViewTracking { hosts: hosts, hostsSanitizer: HostsSanitizer(), logsSampleRate: logsSampleRate, + sessionReplayConfiguration: sessionReplayConfiguration, in: core ) } @@ -74,6 +107,7 @@ public enum WebViewTracking { hosts: Set, hostsSanitizer: HostsSanitizing, logsSampleRate: Float, + sessionReplayConfiguration: SessionReplayConfiguration?, in core: DatadogCoreProtocol ) { let isTracking = controller.userScripts.contains { $0.source.starts(with: Self.jsCodePrefix) } @@ -106,15 +140,26 @@ public enum WebViewTracking { .map { return "\"\($0)\"" } .joined(separator: ",") + let privacyLevel = sessionReplayConfiguration?.privacyLevel ?? .mask + + // Share native capabilities with Browser SDK + let capabilities = sessionReplayConfiguration != nil ? "\"records\"" : "" + let js = """ \(Self.jsCodePrefix) window.\(bridgeName) = { - send(msg) { - \(webkitMethodName)(msg) - }, - getAllowedWebViewHosts() { - return '[\(allowedWebViewHostsString)]' - } + send(msg) { + \(webkitMethodName)(msg) + }, + getAllowedWebViewHosts() { + return '[\(allowedWebViewHostsString)]' + }, + getCapabilities() { + return '[\(capabilities)]' + }, + getPrivacyLevel() { + return '\(privacyLevel.rawValue)' + } } """ diff --git a/DatadogWebViewTracking/Tests/Mocks.swift b/DatadogWebViewTracking/Tests/Mocks.swift new file mode 100644 index 0000000000..36571392dc --- /dev/null +++ b/DatadogWebViewTracking/Tests/Mocks.swift @@ -0,0 +1,56 @@ +/* + * 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 +#if !os(tvOS) +import WebKit +import TestUtilities + +@testable import DatadogWebViewTracking + +final class DDUserContentController: WKUserContentController { + typealias NameHandlerPair = (name: String, handler: WKScriptMessageHandler) + private(set) var messageHandlers = [NameHandlerPair]() + + override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { + messageHandlers.append((name: name, handler: scriptMessageHandler)) + } + + override func removeScriptMessageHandler(forName name: String) { + messageHandlers = messageHandlers.filter { + return $0.name != name + } + } +} + +final class MockMessageHandler: NSObject, WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { } +} + +final class MockScriptMessage: WKScriptMessage { + private let _body: Any + private weak var _webView: WKWebView? + + init(body: Any, webView: WKWebView? = nil) { + _body = body + _webView = webView + } + + override var body: Any { _body } + override weak var webView: WKWebView? { _webView } +} + +extension WebViewTracking.SessionReplayConfiguration.PrivacyLevel: AnyMockable, RandomMockable { + public static func mockAny() -> Self { + .allow + } + + public static func mockRandom() -> Self { + [.allow, .mask, .maskUserInput].randomElement()! + } +} + +#endif diff --git a/DatadogWebViewTracking/Tests/WebViewTrackingTests.swift b/DatadogWebViewTracking/Tests/WebViewTrackingTests.swift index b44a5b4fc0..dfbbdd8a5f 100644 --- a/DatadogWebViewTracking/Tests/WebViewTrackingTests.swift +++ b/DatadogWebViewTracking/Tests/WebViewTrackingTests.swift @@ -12,36 +12,80 @@ import TestUtilities import DatadogInternal @testable import DatadogWebViewTracking -final class DDUserContentController: WKUserContentController { - typealias NameHandlerPair = (name: String, handler: WKScriptMessageHandler) - private(set) var messageHandlers = [NameHandlerPair]() +class WebViewTrackingTests: XCTestCase { + func testItAddsUserScript() throws { + let mockSanitizer = HostsSanitizerMock() + let controller = DDUserContentController() - override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { - messageHandlers.append((name: name, handler: scriptMessageHandler)) - } + let host: String = .mockRandom() - override func removeScriptMessageHandler(forName name: String) { - messageHandlers = messageHandlers.filter { - return $0.name != name + WebViewTracking.enable( + tracking: controller, + hosts: [host], + hostsSanitizer: mockSanitizer, + logsSampleRate: 30, + sessionReplayConfiguration: nil, + in: PassthroughCoreMock() + ) + + let script = try XCTUnwrap(controller.userScripts.last) + XCTAssertEqual(script.source, """ + /* DatadogEventBridge */ + window.DatadogEventBridge = { + send(msg) { + window.webkit.messageHandlers.DatadogEventBridge.postMessage(msg) + }, + getAllowedWebViewHosts() { + return '["\(host)"]' + }, + getCapabilities() { + return '[]' + }, + getPrivacyLevel() { + return 'mask' + } } + """) } -} -final class MockMessageHandler: NSObject, WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { } -} + func testItAddsUserScriptWithSessionReplay() throws { + let mockSanitizer = HostsSanitizerMock() + let controller = DDUserContentController() -final class MockScriptMessage: WKScriptMessage { - let mockBody: Any + let host: String = .mockRandom() + let sessionReplayConfiguration = WebViewTracking.SessionReplayConfiguration( + privacyLevel: .mockRandom() + ) - init(body: Any) { - self.mockBody = body - } + WebViewTracking.enable( + tracking: controller, + hosts: [host], + hostsSanitizer: mockSanitizer, + logsSampleRate: 30, + sessionReplayConfiguration: sessionReplayConfiguration, + in: PassthroughCoreMock() + ) - override var body: Any { return mockBody } -} + let script = try XCTUnwrap(controller.userScripts.last) + XCTAssertEqual(script.source, """ + /* DatadogEventBridge */ + window.DatadogEventBridge = { + send(msg) { + window.webkit.messageHandlers.DatadogEventBridge.postMessage(msg) + }, + getAllowedWebViewHosts() { + return '["\(host)"]' + }, + getCapabilities() { + return '["records"]' + }, + getPrivacyLevel() { + return '\(sessionReplayConfiguration.privacyLevel.rawValue)' + } + } + """) + } -class WebViewTrackingTests: XCTestCase { func testItAddsUserScriptAndMessageHandler() throws { let mockSanitizer = HostsSanitizerMock() let controller = DDUserContentController() @@ -53,6 +97,7 @@ class WebViewTrackingTests: XCTestCase { hosts: ["datadoghq.com"], hostsSanitizer: mockSanitizer, logsSampleRate: 30, + sessionReplayConfiguration: nil, in: PassthroughCoreMock() ) @@ -84,6 +129,7 @@ class WebViewTrackingTests: XCTestCase { hosts: ["datadoghq.com"], hostsSanitizer: mockSanitizer, logsSampleRate: 100, + sessionReplayConfiguration: nil, in: PassthroughCoreMock() ) } @@ -162,6 +208,7 @@ class WebViewTrackingTests: XCTestCase { hosts: ["datadoghq.com"], hostsSanitizer: HostsSanitizerMock(), logsSampleRate: 100, + sessionReplayConfiguration: nil, in: core ) @@ -214,6 +261,7 @@ class WebViewTrackingTests: XCTestCase { hosts: ["datadoghq.com"], hostsSanitizer: HostsSanitizerMock(), logsSampleRate: 100, + sessionReplayConfiguration: nil, in: core ) @@ -281,6 +329,7 @@ class WebViewTrackingTests: XCTestCase { func testSendingWebRecordEvent() throws { let recordMessageExpectation = expectation(description: "Record message received") + let webView = WKWebView() let controller = DDUserContentController() let core = PassthroughCoreMock( @@ -290,7 +339,7 @@ class WebViewTrackingTests: XCTestCase { XCTAssertEqual(view.id, "64308fd4-83f9-48cb-b3e1-1e91f6721230") let matcher = JSONObjectMatcher(object: event) XCTAssertEqual(try? matcher.value("date"), 1_635_932_927_012) - XCTAssertEqual(try? matcher.value("slotId"), "\(controller.hash)") + XCTAssertEqual(try? matcher.value("slotId"), String(webView.hash)) recordMessageExpectation.fulfill() case .context: break @@ -305,6 +354,7 @@ class WebViewTrackingTests: XCTestCase { hosts: ["datadoghq.com"], hostsSanitizer: HostsSanitizerMock(), logsSampleRate: 100, + sessionReplayConfiguration: nil, in: core ) @@ -317,7 +367,7 @@ class WebViewTrackingTests: XCTestCase { }, "view": { "id": "64308fd4-83f9-48cb-b3e1-1e91f6721230" } } - """) + """, webView: webView) messageHandler?.userContentController(controller, didReceive: webLogMessage) waitForExpectations(timeout: 1) diff --git a/IntegrationTests/IntegrationScenarios/IntegrationTests.swift b/IntegrationTests/IntegrationScenarios/IntegrationTests.swift index c75f1f3247..5d4e1cf811 100644 --- a/IntegrationTests/IntegrationScenarios/IntegrationTests.swift +++ b/IntegrationTests/IntegrationScenarios/IntegrationTests.swift @@ -25,7 +25,6 @@ class IntegrationTests: XCTestCase { override func tearDownWithError() throws { server = nil - try super.tearDownWithError() } diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift index 7b4b27a51a..7d028a0905 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMResourcesScenarioTests.swift @@ -409,14 +409,14 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { return .init( idHi: UInt64(traceIDHiValue, radix: 16) ?? 0, - idLo: UInt64(traceIDLoValue, radix: 16) ?? 0 + idLo: UInt64(traceIDLoValue, radix: 10) ?? 0 ) } private func getSpanID(from request: Request) -> SpanID? { guard let spanId = request.httpHeaders["x-datadog-parent-id"] else { return nil } - return .init(spanId, representation: .hexadecimal) + return .init(spanId, representation: .decimal) } private func isValid(sampleRate: Double) -> Bool { sampleRate >= 0 && sampleRate <= 1 } } diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/SessionReplay/SRMultipleViewsRecordingScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/SessionReplay/SRMultipleViewsRecordingScenarioTests.swift index cfb520eced..dfbf461fd1 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/SessionReplay/SRMultipleViewsRecordingScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/SessionReplay/SRMultipleViewsRecordingScenarioTests.swift @@ -48,7 +48,6 @@ class SRMultipleViewsRecordingScenarioTests: IntegrationTests, RUMCommonAsserts, static let totalTouchDataRecords = 10 } - func testSRMultipleViewsRecordingScenario() throws { // RUM endpoint in `HTTPServerMock` let rumEndpoint = server.obtainUniqueRecordingSession() diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift index cc05633e74..5edd75dd86 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/Tracing/TracingURLSessionScenarioTests.swift @@ -238,8 +238,9 @@ class TracingURLSessionScenarioTests: IntegrationTests, TracingCommonAsserts { XCTAssertEqual(firstPartyRequests.count, 1) let firstPartyRequest = firstPartyRequests[0] - XCTAssertEqual(firstPartyRequest.httpHeaders["x-datadog-trace-id"], try taskWithRequest.traceID()?.idLoHex) - XCTAssertEqual(firstPartyRequest.httpHeaders["x-datadog-parent-id"], try taskWithRequest.spanID()?.toString(representation: .hexadecimal)) + let traceId = try taskWithRequest.traceID() ?? .invalid + XCTAssertEqual(firstPartyRequest.httpHeaders["x-datadog-trace-id"], String(traceId.idLo)) + XCTAssertEqual(firstPartyRequest.httpHeaders["x-datadog-parent-id"], try taskWithRequest.spanID()?.toString(representation: .decimal)) XCTAssertEqual(firstPartyRequest.httpHeaders["x-datadog-sampling-priority"], "1") XCTAssertNil(firstPartyRequest.httpHeaders["x-datadog-origin"]) let tid = try taskWithRequest.meta.tid() diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/WebView/WebViewScenarioTest.swift b/IntegrationTests/IntegrationScenarios/Scenarios/WebView/WebViewScenarioTest.swift index 51b22c3abb..fb6c5d04bd 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/WebView/WebViewScenarioTest.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/WebView/WebViewScenarioTest.swift @@ -69,7 +69,6 @@ class WebViewScenarioTest: IntegrationTests, RUMCommonAsserts { XCTAssertEqual(browserViewEvent.session.id, expectedBrowserSessionID, "Webview events should use iOS SDK session ID") XCTAssertEqual(browserViewEvent.service, expectedBrowserServiceName, "Webview events should use Browser SDK `service`") XCTAssertEqual(browserViewEvent.source, .browser, "Webview events should use Browser SDK `source`") - XCTAssertEqual(browserViewEvent.dd.session?.plan, .plan1, "Webview events should use iOS SDK plan 1") XCTAssertEqual(browserViewEvent.container?.source, .ios, "Webview events should include a container source") XCTAssertEqual(browserViewEvent.container?.view.id, expectedBrowserContainerViewID, "Webview events should include a container view.id") } @@ -79,7 +78,6 @@ class WebViewScenarioTest: IntegrationTests, RUMCommonAsserts { XCTAssertEqual(browserResourceEvent.session.id, expectedBrowserSessionID, "Webview events should use iOS SDK session ID") XCTAssertEqual(browserResourceEvent.service, expectedBrowserServiceName, "Webview events should use Browser SDK `service`") XCTAssertEqual(browserResourceEvent.source, .browser, "Webview events should use Browser SDK `source`") - XCTAssertEqual(browserResourceEvent.dd.session?.plan, .plan1, "Webview events should use iOS SDK plan 1") XCTAssertEqual(browserResourceEvent.container?.source, .ios, "Webview events should use include a container view") XCTAssertEqual(browserResourceEvent.container?.view.id, expectedBrowserContainerViewID, "Webview events should include a container view.id") } diff --git a/Makefile b/Makefile index 5e743d7134..be0c55047e 100644 --- a/Makefile +++ b/Makefile @@ -180,6 +180,7 @@ api-surface: --library-name DatadogRUM \ --library-name DatadogCrashReporting \ --library-name DatadogWebViewTracking \ + --library-name DatadogSessionReplay \ > ../../api-surface-swift && \ cd - diff --git a/Package.swift b/Package.swift index 5dc10e2db5..87c819d351 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,8 @@ let package = Package( name: "Datadog", platforms: [ .iOS(.v11), - .tvOS(.v11) + .tvOS(.v11), + .macOS(.v12) ], products: [ .library( diff --git a/README.md b/README.md index e239fd3ca0..145e0b182e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ +

+ + + + + + + + + + + + +

+ + # Datadog SDK for iOS and tvOS > Swift and Objective-C libraries to interact with Datadog on iOS and tvOS. diff --git a/SUPPORTED-VERSIONS.md b/SUPPORTED-VERSIONS.md index 69d08a31e2..6f56125caa 100644 --- a/SUPPORTED-VERSIONS.md +++ b/SUPPORTED-VERSIONS.md @@ -5,13 +5,24 @@ | **iOS** | ✅ | `11+` | | **tvOS** | ✅ | `11+` | | **iPadOS** | ✅ | `11+` | -| **VisionOS** | ⚠️ | `1.1+` | +| **macOS (Designed for iPad)** | ✅ | `11+` | +| **macOS (Catalyst)** | ⚠️ | `12+` | +| **macOS** | ⚠️ | `12+` | +| **visionOS** | ⚠️ | `1.0+` | | **watchOS**| ❌ | `n/a` | -| **macOS** | ❌ | `n/a` | | **Linux** | ❌ | `n/a` | ## VisionOS -VisionOS is not officially supported by Datadog SDK. Some features may not be fully functional. + +VisionOS is not officially supported by Datadog SDK. Some features may not be fully functional. Note that `DatadogCrashReporting` is not supported on VisionOS, due to lack of support on the [PLCrashReporter side](https://github.com/microsoft/plcrashreporter/issues/288). + +## MacOS + +MacOS is not officially supported by Datadog SDK. Some features may not be fully functional. Note that `DatadogRUM`, `DatadogSessionReplay` and `DatadogObjc` which heavily depend on `UIKit` do not build on macOS. + +## Catalyst + +We support Catalyst in build mode only, which means that macOS target will build, but functionalities for the SDK might not work for this target. ## Xcode @@ -47,9 +58,6 @@ We currently support integration of the SDK using following dependency managers. *Note: Third party networking libraries can be instrumented by implementing custom `DDURLSessionDelegate`.* -## Catalyst -We support Catalyst in build mode only, which means that macOS target will build, but functionalities for the SDK won't work for this target. - ## Dependencies The Datadog SDK depends on the following third-party library: -- [PLCrashReporter](https://github.com/microsoft/plcrashreporter) 1.11.1 \ No newline at end of file +- [PLCrashReporter](https://github.com/microsoft/plcrashreporter) 1.11.1 diff --git a/TestUtilities/Helpers/DDAssert.swift b/TestUtilities/Helpers/DDAssert.swift index 9ea496044d..3a5c829c6a 100644 --- a/TestUtilities/Helpers/DDAssert.swift +++ b/TestUtilities/Helpers/DDAssert.swift @@ -72,8 +72,8 @@ private func _DDAssertReflectionEqual(_ expression1: @autoclosure () throws -> A switch (mirror1.displayStyle, mirror2.displayStyle) { case (.dictionary?, .dictionary?): // two dictionaries - let dictionary1 = value1 as! [String: Any] - let dictionary2 = value2 as! [String: Any] + let dictionary1 = value1 as! [AnyHashable: Any] + let dictionary2 = value2 as! [AnyHashable: Any] guard dictionary1.keys.count == dictionary2.keys.count else { throw DDAssertError.expectedFailure("dictionaries have different number of keys", keyPath: keyPath) @@ -84,7 +84,7 @@ private func _DDAssertReflectionEqual(_ expression1: @autoclosure () throws -> A throw DDAssertError.expectedFailure("dictionaries have different key names", keyPath: keyPath) } - try _DDAssertReflectionEqual(value1, value2, keyPath: keyPath + [key1]) + try _DDAssertReflectionEqual(value1, value2, keyPath: keyPath + [key1.description]) } return // dictionaries are equal diff --git a/TestUtilities/Mocks/FoundationMocks.swift b/TestUtilities/Mocks/FoundationMocks.swift index 682b89f7cc..d2bc014c18 100644 --- a/TestUtilities/Mocks/FoundationMocks.swift +++ b/TestUtilities/Mocks/FoundationMocks.swift @@ -158,6 +158,10 @@ extension Dictionary: RandomMockable where Key: RandomMockable, Value: RandomMoc public static func mockRandom() -> Dictionary { return [Key.mockRandom(): Value.mockRandom()] } + + public static func mockRandom(count: Int) -> Dictionary { + return (0.. Set { return Set([Element].mockRandom()) } + + public static func mockRandom(count: Int) -> Set { + return Set([Element].mockRandom(count: count)) + } } extension Date: AnyMockable, RandomMockable { @@ -620,7 +628,7 @@ extension URLSessionTask { return URLSessionDataTaskMock(request: .mockAny(), response: .mockAny()) } - public static func mockWith(request: URLRequest, response: HTTPURLResponse) -> URLSessionDataTask { + public static func mockWith(request: URLRequest = .mockAny(), response: HTTPURLResponse = .mockAny()) -> URLSessionDataTask { return URLSessionDataTaskMock(request: request, response: response) } } diff --git a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift index 9d88f1008a..23b9c61290 100644 --- a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift +++ b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift @@ -7,22 +7,30 @@ import Foundation import DatadogInternal -extension SpanID { +extension SpanID: AnyMockable, RandomMockable { public static func mockAny() -> SpanID { return SpanID(rawValue: .mockAny()) } + public static func mockRandom() -> SpanID { + return SpanID(rawValue: .mockRandom()) + } + public static func mock(_ rawValue: UInt64) -> SpanID { return SpanID(rawValue: rawValue) } } -extension TraceID { +extension TraceID: AnyMockable, RandomMockable { public static func mockAny() -> TraceID { return TraceID(rawValue: (.mockAny(), .mockAny())) } + public static func mockRandom() -> TraceID { + return TraceID(idHi: .mockRandom(), idLo: .mockRandom()) + } + public static func mock(_ rawValue: (UInt64, UInt64)) -> TraceID { return TraceID(rawValue: rawValue) } @@ -36,6 +44,38 @@ extension TraceID { } } +extension TraceContext: AnyMockable, RandomMockable { + public static func mockAny() -> TraceContext { + return .mockWith() + } + + public static func mockRandom() -> TraceContext { + return .mockWith( + traceID: .mockRandom(), + spanID: .mockRandom(), + parentSpanID: .mockRandom(), + sampleRate: .mockRandom(min: 0, max: 100), + isKept: .random() + ) + } + + public static func mockWith( + traceID: TraceID = .mockAny(), + spanID: SpanID = .mockAny(), + parentSpanID: SpanID? = nil, + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() + ) -> TraceContext { + return TraceContext( + traceID: traceID, + spanID: spanID, + parentSpanID: parentSpanID, + sampleRate: sampleRate, + isKept: isKept + ) + } +} + public class RelativeTracingUUIDGenerator: TraceIDGenerator { private(set) var uuid: TraceID internal let count: UInt64 @@ -88,7 +128,7 @@ public final class URLSessionHandlerMock: DatadogURLSessionHandler { public var firstPartyHosts: FirstPartyHosts public var modifiedRequest: URLRequest? - public var parentSpan: TraceContext? + public var injectedTraceContext: TraceContext? public var shouldInterceptRequest: ((URLRequest) -> Bool)? public var onRequestMutation: ((URLRequest, Set) -> Void)? @@ -111,13 +151,9 @@ public final class URLSessionHandlerMock: DatadogURLSessionHandler { interceptions.values.first { $0.request.url == url } } - public func modify(request: URLRequest, headerTypes: Set) -> URLRequest { + public func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { onRequestMutation?(request, headerTypes) - return modifiedRequest ?? request - } - - public func traceContext() -> TraceContext? { - parentSpan + return (modifiedRequest ?? request, injectedTraceContext) } public func interceptionDidStart(interception: URLSessionTaskInterception) { diff --git a/api-surface-objc b/api-surface-objc index 874866f013..75fe730a55 100644 --- a/api-surface-objc +++ b/api-surface-objc @@ -2,6 +2,77 @@ # API surface for DatadogObjc: # ---------------------------------- +open class DDNSURLSessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate + override public init() + public init(additionalFirstPartyHostsWithHeaderTypes: [String: Set]) + public convenience init(additionalFirstPartyHosts: Set) +public class DDURLSessionInstrumentationConfiguration: NSObject + public init(delegateClass: URLSessionDataDelegate.Type) + public func setFirstPartyHostsTracing(_ firstPartyHostsTracing: DDURLSessionInstrumentationFirstPartyHostsTracing) + @objc public var delegateClass: URLSessionDataDelegate.Type +public class DDURLSessionInstrumentationFirstPartyHostsTracing: NSObject + public init(hostsWithHeaderTypes: [String: Set]) + public init(hosts: Set) +public class DDURLSessionInstrumentation: NSObject + public static func enable(configuration: DDURLSessionInstrumentationConfiguration) + public static func disable(delegateClass: URLSessionDataDelegate.Type) +public class DDTrackingConsent: NSObject + public static func granted() -> DDTrackingConsent + public static func notGranted() -> DDTrackingConsent + public static func pending() -> DDTrackingConsent +public class DDDatadog: NSObject + public static func initialize(configuration: DDConfiguration,trackingConsent: DDTrackingConsent) + public static func setVerbosityLevel(_ verbosityLevel: DDSDKVerbosityLevel) + public static func verbosityLevel() -> DDSDKVerbosityLevel + public static func setUserInfo(id: String? = nil, name: String? = nil, email: String? = nil, extraInfo: [String: Any] = [:]) + public static func addUserExtraInfo(_ extraInfo: [String: Any]) + public static func setTrackingConsent(consent: DDTrackingConsent) + public static func isInitialized() -> Bool + public static func stopInstance() + public static func clearAllData() +public class DDSite: NSObject + public static func us1() -> DDSite + public static func us3() -> DDSite + public static func us5() -> DDSite + public static func eu1() -> DDSite + public static func ap1() -> DDSite + public static func us1_fed() -> DDSite +public enum DDBatchSize: Int + case small + case medium + case large +public enum DDUploadFrequency: Int + case frequent + case average + case rare +public enum DDBatchProcessingLevel: Int + case low + case medium + case high +public class DDTracingHeaderType: NSObject + @objc public static let datadog = DDTracingHeaderType(.datadog) + @objc public static let b3multi = DDTracingHeaderType(.b3multi) + @objc public static let b3 = DDTracingHeaderType(.b3) + @objc public static let tracecontext = DDTracingHeaderType(.tracecontext) +public protocol DDDataEncryption: AnyObject + func encrypt(data: Data) throws -> Data + func decrypt(data: Data) throws -> Data +public protocol DDServerDateProvider: AnyObject + func synchronize(update: @escaping (TimeInterval) -> Void) +public class DDConfiguration: NSObject + @objc public var clientToken: String + @objc public var env: String + @objc public var site: DDSite + @objc public var service: String? + @objc public var batchSize: DDBatchSize + @objc public var uploadFrequency: DDUploadFrequency + @objc public var batchProcessingLevel: DDBatchProcessingLevel + @objc public var proxyConfiguration: [AnyHashable: Any]? + public func setEncryption(_ encryption: DDDataEncryption) + public func setServerDateProvider(_ serverDateProvider: DDServerDateProvider) + @objc public var bundle: Bundle + @objc public var additionalConfiguration: [String: Any] + public init(clientToken: String, env: String) public enum DDSDKVerbosityLevel: Int case none case debug @@ -56,62 +127,6 @@ public class DDLogger: NSObject public func add(tag: String) public func remove(tag: String) public static func create(with configuration: DDLoggerConfiguration = .init()) -> DDLogger -open class DDNSURLSessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate, __URLSessionDelegateProviding - public var ddURLSessionDelegate: DatadogURLSessionDelegate - override public init() - public init(additionalFirstPartyHostsWithHeaderTypes: [String: Set]) - public init(additionalFirstPartyHosts: Set) - open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) - open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) - open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) -public class DDTrackingConsent: NSObject - public static func granted() -> DDTrackingConsent - public static func notGranted() -> DDTrackingConsent - public static func pending() -> DDTrackingConsent -public class DDDatadog: NSObject - public static func initialize(configuration: DDConfiguration,trackingConsent: DDTrackingConsent) - public static func setVerbosityLevel(_ verbosityLevel: DDSDKVerbosityLevel) - public static func verbosityLevel() -> DDSDKVerbosityLevel - public static func setUserInfo(id: String? = nil, name: String? = nil, email: String? = nil, extraInfo: [String: Any] = [:]) - public static func setTrackingConsent(consent: DDTrackingConsent) - public static func clearAllData() -public class DDSite: NSObject - public static func us1() -> DDSite - public static func us3() -> DDSite - public static func us5() -> DDSite - public static func eu1() -> DDSite - public static func ap1() -> DDSite - public static func us1_fed() -> DDSite -public enum DDBatchSize: Int - case small - case medium - case large -public enum DDUploadFrequency: Int - case frequent - case average - case rare -public class DDTracingHeaderType: NSObject - @objc public static let datadog = DDTracingHeaderType(.datadog) - @objc public static let b3multi = DDTracingHeaderType(.b3multi) - @objc public static let b3 = DDTracingHeaderType(.b3) - @objc public static let tracecontext = DDTracingHeaderType(.tracecontext) -public protocol DDDataEncryption: AnyObject - func encrypt(data: Data) throws -> Data - func decrypt(data: Data) throws -> Data -public protocol DDServerDateProvider: AnyObject - func synchronize(update: @escaping (TimeInterval) -> Void) -public class DDConfiguration: NSObject - @objc public var clientToken: String - @objc public var env: String - @objc public var site: DDSite - @objc public var service: String? - @objc public var batchSize: DDBatchSize - @objc public var uploadFrequency: DDUploadFrequency - @objc public var proxyConfiguration: [AnyHashable: Any]? - public func setEncryption(_ encryption: DDDataEncryption) - public func setServerDateProvider(_ serverDateProvider: DDServerDateProvider) - @objc public var bundle: Bundle - public init(clientToken: String, env: String) public protocol OTSpan var context: OTSpanContext var tracer: OTTracer @@ -146,6 +161,10 @@ public class DDRUMView: NSObject public init(name: String, attributes: [String: Any]) public protocol DDUIKitRUMViewsPredicate: AnyObject func rumView(for viewController: UIViewController) -> DDRUMView? +public class DDDefaultUIKitRUMViewsPredicate: NSObject, DDUIKitRUMViewsPredicate + public func rumView(for viewController: UIViewController) -> DDRUMView? +public class DDDefaultUIKitRUMActionsPredicate: NSObject, DDUIKitRUMActionsPredicate + public func rumAction(targetView: UIView) -> DDRUMAction? public class DDRUMAction: NSObject @objc public var name: String @objc public var attributes: [String: Any] @@ -246,8 +265,11 @@ public class DDRUMActionEvent: NSObject @objc public var dd: DDRUMActionEventDD @objc public var action: DDRUMActionEventAction @objc public var application: DDRUMActionEventApplication + @objc public var buildId: String? + @objc public var buildVersion: String? @objc public var ciTest: DDRUMActionEventRUMCITest? @objc public var connectivity: DDRUMActionEventRUMConnectivity? + @objc public var container: DDRUMActionEventContainer? @objc public var context: DDRUMActionEventRUMEventAttributes? @objc public var date: NSNumber @objc public var device: DDRUMActionEventRUMDevice? @@ -256,7 +278,7 @@ public class DDRUMActionEvent: NSObject @objc public var service: String? @objc public var session: DDRUMActionEventSession @objc public var source: DDRUMActionEventSource - @objc public var synthetics: DDRUMActionEventSynthetics? + @objc public var synthetics: DDRUMActionEventRUMSyntheticsTest? @objc public var type: String @objc public var usr: DDRUMActionEventRUMUser? @objc public var version: String? @@ -264,6 +286,7 @@ public class DDRUMActionEvent: NSObject public class DDRUMActionEventDD: NSObject @objc public var action: DDRUMActionEventDDAction? @objc public var browserSdkVersion: String? + @objc public var configuration: DDRUMActionEventDDConfiguration? @objc public var formatVersion: NSNumber @objc public var session: DDRUMActionEventDDSession? public class DDRUMActionEventDDAction: NSObject @@ -276,12 +299,25 @@ public class DDRUMActionEventDDActionTarget: NSObject @objc public var height: NSNumber? @objc public var selector: String? @objc public var width: NSNumber? +public class DDRUMActionEventDDConfiguration: NSObject + @objc public var sessionReplaySampleRate: NSNumber? + @objc public var sessionSampleRate: NSNumber public class DDRUMActionEventDDSession: NSObject @objc public var plan: DDRUMActionEventDDSessionPlan + @objc public var sessionPrecondition: DDRUMActionEventDDSessionRUMSessionPrecondition public enum DDRUMActionEventDDSessionPlan: Int case none case plan1 case plan2 +public enum DDRUMActionEventDDSessionRUMSessionPrecondition: Int + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop public class DDRUMActionEventAction: NSObject @objc public var crash: DDRUMActionEventActionCrash? @objc public var error: DDRUMActionEventActionError? @@ -324,12 +360,20 @@ public class DDRUMActionEventRUMCITest: NSObject @objc public var testExecutionId: String public class DDRUMActionEventRUMConnectivity: NSObject @objc public var cellular: DDRUMActionEventRUMConnectivityCellular? - @objc public var interfaces: [Int] + @objc public var effectiveType: DDRUMActionEventRUMConnectivityEffectiveType + @objc public var interfaces: [Int]? @objc public var status: DDRUMActionEventRUMConnectivityStatus public class DDRUMActionEventRUMConnectivityCellular: NSObject @objc public var carrierName: String? @objc public var technology: String? +public enum DDRUMActionEventRUMConnectivityEffectiveType: Int + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g public enum DDRUMActionEventRUMConnectivityInterfaces: Int + case none case bluetooth case cellular case ethernet @@ -338,11 +382,24 @@ public enum DDRUMActionEventRUMConnectivityInterfaces: Int case mixed case other case unknown - case none + case interfacesNone public enum DDRUMActionEventRUMConnectivityStatus: Int case connected case notConnected case maybe +public class DDRUMActionEventContainer: NSObject + @objc public var source: DDRUMActionEventContainerSource + @objc public var view: DDRUMActionEventContainerView +public enum DDRUMActionEventContainerSource: Int + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMActionEventContainerView: NSObject + @objc public var id: String public class DDRUMActionEventRUMEventAttributes: NSObject @objc public var contextInfo: [String: Any] public class DDRUMActionEventRUMDevice: NSObject @@ -372,8 +429,8 @@ public class DDRUMActionEventRUMOperatingSystem: NSObject public class DDRUMActionEventSession: NSObject @objc public var hasReplay: NSNumber? @objc public var id: String - @objc public var type: DDRUMActionEventSessionSessionType -public enum DDRUMActionEventSessionSessionType: Int + @objc public var type: DDRUMActionEventSessionRUMSessionType +public enum DDRUMActionEventSessionRUMSessionType: Int case user case synthetics case ciTest @@ -385,7 +442,8 @@ public enum DDRUMActionEventSource: Int case flutter case reactNative case roku -public class DDRUMActionEventSynthetics: NSObject + case unity +public class DDRUMActionEventRUMSyntheticsTest: NSObject @objc public var injected: NSNumber? @objc public var resultId: String @objc public var testId: String @@ -404,33 +462,51 @@ public class DDRUMErrorEvent: NSObject @objc public var dd: DDRUMErrorEventDD @objc public var action: DDRUMErrorEventAction? @objc public var application: DDRUMErrorEventApplication + @objc public var buildId: String? + @objc public var buildVersion: String? @objc public var ciTest: DDRUMErrorEventRUMCITest? @objc public var connectivity: DDRUMErrorEventRUMConnectivity? + @objc public var container: DDRUMErrorEventContainer? @objc public var context: DDRUMErrorEventRUMEventAttributes? @objc public var date: NSNumber @objc public var device: DDRUMErrorEventRUMDevice? @objc public var display: DDRUMErrorEventDisplay? @objc public var error: DDRUMErrorEventError @objc public var featureFlags: DDRUMErrorEventFeatureFlags? + @objc public var freeze: DDRUMErrorEventFreeze? @objc public var os: DDRUMErrorEventRUMOperatingSystem? @objc public var service: String? @objc public var session: DDRUMErrorEventSession @objc public var source: DDRUMErrorEventSource - @objc public var synthetics: DDRUMErrorEventSynthetics? + @objc public var synthetics: DDRUMErrorEventRUMSyntheticsTest? @objc public var type: String @objc public var usr: DDRUMErrorEventRUMUser? @objc public var version: String? @objc public var view: DDRUMErrorEventView public class DDRUMErrorEventDD: NSObject @objc public var browserSdkVersion: String? + @objc public var configuration: DDRUMErrorEventDDConfiguration? @objc public var formatVersion: NSNumber @objc public var session: DDRUMErrorEventDDSession? +public class DDRUMErrorEventDDConfiguration: NSObject + @objc public var sessionReplaySampleRate: NSNumber? + @objc public var sessionSampleRate: NSNumber public class DDRUMErrorEventDDSession: NSObject @objc public var plan: DDRUMErrorEventDDSessionPlan + @objc public var sessionPrecondition: DDRUMErrorEventDDSessionRUMSessionPrecondition public enum DDRUMErrorEventDDSessionPlan: Int case none case plan1 case plan2 +public enum DDRUMErrorEventDDSessionRUMSessionPrecondition: Int + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop public class DDRUMErrorEventAction: NSObject @objc public var id: DDRUMErrorEventActionRUMActionID public class DDRUMErrorEventActionRUMActionID: NSObject @@ -442,12 +518,20 @@ public class DDRUMErrorEventRUMCITest: NSObject @objc public var testExecutionId: String public class DDRUMErrorEventRUMConnectivity: NSObject @objc public var cellular: DDRUMErrorEventRUMConnectivityCellular? - @objc public var interfaces: [Int] + @objc public var effectiveType: DDRUMErrorEventRUMConnectivityEffectiveType + @objc public var interfaces: [Int]? @objc public var status: DDRUMErrorEventRUMConnectivityStatus public class DDRUMErrorEventRUMConnectivityCellular: NSObject @objc public var carrierName: String? @objc public var technology: String? +public enum DDRUMErrorEventRUMConnectivityEffectiveType: Int + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g public enum DDRUMErrorEventRUMConnectivityInterfaces: Int + case none case bluetooth case cellular case ethernet @@ -456,11 +540,24 @@ public enum DDRUMErrorEventRUMConnectivityInterfaces: Int case mixed case other case unknown - case none + case interfacesNone public enum DDRUMErrorEventRUMConnectivityStatus: Int case connected case notConnected case maybe +public class DDRUMErrorEventContainer: NSObject + @objc public var source: DDRUMErrorEventContainerSource + @objc public var view: DDRUMErrorEventContainerView +public enum DDRUMErrorEventContainerSource: Int + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMErrorEventContainerView: NSObject + @objc public var id: String public class DDRUMErrorEventRUMEventAttributes: NSObject @objc public var contextInfo: [String: Any] public class DDRUMErrorEventRUMDevice: NSObject @@ -483,6 +580,8 @@ public class DDRUMErrorEventDisplayViewport: NSObject @objc public var height: NSNumber @objc public var width: NSNumber public class DDRUMErrorEventError: NSObject + @objc public var binaryImages: [DDRUMErrorEventErrorBinaryImages]? + @objc public var category: DDRUMErrorEventErrorCategory @objc public var causes: [DDRUMErrorEventErrorCauses]? @objc public var fingerprint: String? @objc public var handling: DDRUMErrorEventErrorHandling @@ -490,11 +589,26 @@ public class DDRUMErrorEventError: NSObject @objc public var id: String? @objc public var isCrash: NSNumber? @objc public var message: String + @objc public var meta: DDRUMErrorEventErrorMeta? @objc public var resource: DDRUMErrorEventErrorResource? @objc public var source: DDRUMErrorEventErrorSource @objc public var sourceType: DDRUMErrorEventErrorSourceType @objc public var stack: String? + @objc public var threads: [DDRUMErrorEventErrorThreads]? @objc public var type: String? + @objc public var wasTruncated: NSNumber? +public class DDRUMErrorEventErrorBinaryImages: NSObject + @objc public var arch: String? + @objc public var isSystem: NSNumber + @objc public var loadAddress: String? + @objc public var maxAddress: String? + @objc public var name: String + @objc public var uuid: String +public enum DDRUMErrorEventErrorCategory: Int + case none + case aNR + case appHang + case exception public class DDRUMErrorEventErrorCauses: NSObject @objc public var message: String @objc public var source: DDRUMErrorEventErrorCausesSource @@ -513,6 +627,14 @@ public enum DDRUMErrorEventErrorHandling: Int case none case handled case unhandled +public class DDRUMErrorEventErrorMeta: NSObject + @objc public var codeType: String? + @objc public var exceptionCodes: String? + @objc public var exceptionType: String? + @objc public var incidentIdentifier: String? + @objc public var parentProcess: String? + @objc public var path: String? + @objc public var process: String? public class DDRUMErrorEventErrorResource: NSObject @objc public var method: DDRUMErrorEventErrorResourceRUMMethod @objc public var provider: DDRUMErrorEventErrorResourceProvider? @@ -525,6 +647,9 @@ public enum DDRUMErrorEventErrorResourceRUMMethod: Int case put case delete case patch + case trace + case options + case connect public class DDRUMErrorEventErrorResourceProvider: NSObject @objc public var domain: String? @objc public var name: String? @@ -562,8 +687,18 @@ public enum DDRUMErrorEventErrorSourceType: Int case reactNative case flutter case roku + case ndk + case iosIl2cpp + case ndkIl2cpp +public class DDRUMErrorEventErrorThreads: NSObject + @objc public var crashed: NSNumber + @objc public var name: String + @objc public var stack: String + @objc public var state: String? public class DDRUMErrorEventFeatureFlags: NSObject @objc public var featureFlagsInfo: [String: Any] +public class DDRUMErrorEventFreeze: NSObject + @objc public var duration: NSNumber public class DDRUMErrorEventRUMOperatingSystem: NSObject @objc public var build: String? @objc public var name: String @@ -572,8 +707,8 @@ public class DDRUMErrorEventRUMOperatingSystem: NSObject public class DDRUMErrorEventSession: NSObject @objc public var hasReplay: NSNumber? @objc public var id: String - @objc public var type: DDRUMErrorEventSessionSessionType -public enum DDRUMErrorEventSessionSessionType: Int + @objc public var type: DDRUMErrorEventSessionRUMSessionType +public enum DDRUMErrorEventSessionRUMSessionType: Int case user case synthetics case ciTest @@ -585,7 +720,8 @@ public enum DDRUMErrorEventSource: Int case flutter case reactNative case roku -public class DDRUMErrorEventSynthetics: NSObject + case unity +public class DDRUMErrorEventRUMSyntheticsTest: NSObject @objc public var injected: NSNumber? @objc public var resultId: String @objc public var testId: String @@ -604,8 +740,11 @@ public class DDRUMLongTaskEvent: NSObject @objc public var dd: DDRUMLongTaskEventDD @objc public var action: DDRUMLongTaskEventAction? @objc public var application: DDRUMLongTaskEventApplication + @objc public var buildId: String? + @objc public var buildVersion: String? @objc public var ciTest: DDRUMLongTaskEventRUMCITest? @objc public var connectivity: DDRUMLongTaskEventRUMConnectivity? + @objc public var container: DDRUMLongTaskEventContainer? @objc public var context: DDRUMLongTaskEventRUMEventAttributes? @objc public var date: NSNumber @objc public var device: DDRUMLongTaskEventRUMDevice? @@ -615,22 +754,36 @@ public class DDRUMLongTaskEvent: NSObject @objc public var service: String? @objc public var session: DDRUMLongTaskEventSession @objc public var source: DDRUMLongTaskEventSource - @objc public var synthetics: DDRUMLongTaskEventSynthetics? + @objc public var synthetics: DDRUMLongTaskEventRUMSyntheticsTest? @objc public var type: String @objc public var usr: DDRUMLongTaskEventRUMUser? @objc public var version: String? @objc public var view: DDRUMLongTaskEventView public class DDRUMLongTaskEventDD: NSObject @objc public var browserSdkVersion: String? + @objc public var configuration: DDRUMLongTaskEventDDConfiguration? @objc public var discarded: NSNumber? @objc public var formatVersion: NSNumber @objc public var session: DDRUMLongTaskEventDDSession? +public class DDRUMLongTaskEventDDConfiguration: NSObject + @objc public var sessionReplaySampleRate: NSNumber? + @objc public var sessionSampleRate: NSNumber public class DDRUMLongTaskEventDDSession: NSObject @objc public var plan: DDRUMLongTaskEventDDSessionPlan + @objc public var sessionPrecondition: DDRUMLongTaskEventDDSessionRUMSessionPrecondition public enum DDRUMLongTaskEventDDSessionPlan: Int case none case plan1 case plan2 +public enum DDRUMLongTaskEventDDSessionRUMSessionPrecondition: Int + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop public class DDRUMLongTaskEventAction: NSObject @objc public var id: DDRUMLongTaskEventActionRUMActionID public class DDRUMLongTaskEventActionRUMActionID: NSObject @@ -642,12 +795,20 @@ public class DDRUMLongTaskEventRUMCITest: NSObject @objc public var testExecutionId: String public class DDRUMLongTaskEventRUMConnectivity: NSObject @objc public var cellular: DDRUMLongTaskEventRUMConnectivityCellular? - @objc public var interfaces: [Int] + @objc public var effectiveType: DDRUMLongTaskEventRUMConnectivityEffectiveType + @objc public var interfaces: [Int]? @objc public var status: DDRUMLongTaskEventRUMConnectivityStatus public class DDRUMLongTaskEventRUMConnectivityCellular: NSObject @objc public var carrierName: String? @objc public var technology: String? +public enum DDRUMLongTaskEventRUMConnectivityEffectiveType: Int + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g public enum DDRUMLongTaskEventRUMConnectivityInterfaces: Int + case none case bluetooth case cellular case ethernet @@ -656,11 +817,24 @@ public enum DDRUMLongTaskEventRUMConnectivityInterfaces: Int case mixed case other case unknown - case none + case interfacesNone public enum DDRUMLongTaskEventRUMConnectivityStatus: Int case connected case notConnected case maybe +public class DDRUMLongTaskEventContainer: NSObject + @objc public var source: DDRUMLongTaskEventContainerSource + @objc public var view: DDRUMLongTaskEventContainerView +public enum DDRUMLongTaskEventContainerSource: Int + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMLongTaskEventContainerView: NSObject + @objc public var id: String public class DDRUMLongTaskEventRUMEventAttributes: NSObject @objc public var contextInfo: [String: Any] public class DDRUMLongTaskEventRUMDevice: NSObject @@ -694,8 +868,8 @@ public class DDRUMLongTaskEventRUMOperatingSystem: NSObject public class DDRUMLongTaskEventSession: NSObject @objc public var hasReplay: NSNumber? @objc public var id: String - @objc public var type: DDRUMLongTaskEventSessionSessionType -public enum DDRUMLongTaskEventSessionSessionType: Int + @objc public var type: DDRUMLongTaskEventSessionRUMSessionType +public enum DDRUMLongTaskEventSessionRUMSessionType: Int case user case synthetics case ciTest @@ -707,7 +881,8 @@ public enum DDRUMLongTaskEventSource: Int case flutter case reactNative case roku -public class DDRUMLongTaskEventSynthetics: NSObject + case unity +public class DDRUMLongTaskEventRUMSyntheticsTest: NSObject @objc public var injected: NSNumber? @objc public var resultId: String @objc public var testId: String @@ -725,8 +900,11 @@ public class DDRUMResourceEvent: NSObject @objc public var dd: DDRUMResourceEventDD @objc public var action: DDRUMResourceEventAction? @objc public var application: DDRUMResourceEventApplication + @objc public var buildId: String? + @objc public var buildVersion: String? @objc public var ciTest: DDRUMResourceEventRUMCITest? @objc public var connectivity: DDRUMResourceEventRUMConnectivity? + @objc public var container: DDRUMResourceEventContainer? @objc public var context: DDRUMResourceEventRUMEventAttributes? @objc public var date: NSNumber @objc public var device: DDRUMResourceEventRUMDevice? @@ -736,25 +914,39 @@ public class DDRUMResourceEvent: NSObject @objc public var service: String? @objc public var session: DDRUMResourceEventSession @objc public var source: DDRUMResourceEventSource - @objc public var synthetics: DDRUMResourceEventSynthetics? + @objc public var synthetics: DDRUMResourceEventRUMSyntheticsTest? @objc public var type: String @objc public var usr: DDRUMResourceEventRUMUser? @objc public var version: String? @objc public var view: DDRUMResourceEventView public class DDRUMResourceEventDD: NSObject @objc public var browserSdkVersion: String? + @objc public var configuration: DDRUMResourceEventDDConfiguration? @objc public var discarded: NSNumber? @objc public var formatVersion: NSNumber @objc public var rulePsr: NSNumber? @objc public var session: DDRUMResourceEventDDSession? @objc public var spanId: String? @objc public var traceId: String? +public class DDRUMResourceEventDDConfiguration: NSObject + @objc public var sessionReplaySampleRate: NSNumber? + @objc public var sessionSampleRate: NSNumber public class DDRUMResourceEventDDSession: NSObject @objc public var plan: DDRUMResourceEventDDSessionPlan + @objc public var sessionPrecondition: DDRUMResourceEventDDSessionRUMSessionPrecondition public enum DDRUMResourceEventDDSessionPlan: Int case none case plan1 case plan2 +public enum DDRUMResourceEventDDSessionRUMSessionPrecondition: Int + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop public class DDRUMResourceEventAction: NSObject @objc public var id: DDRUMResourceEventActionRUMActionID public class DDRUMResourceEventActionRUMActionID: NSObject @@ -766,12 +958,20 @@ public class DDRUMResourceEventRUMCITest: NSObject @objc public var testExecutionId: String public class DDRUMResourceEventRUMConnectivity: NSObject @objc public var cellular: DDRUMResourceEventRUMConnectivityCellular? - @objc public var interfaces: [Int] + @objc public var effectiveType: DDRUMResourceEventRUMConnectivityEffectiveType + @objc public var interfaces: [Int]? @objc public var status: DDRUMResourceEventRUMConnectivityStatus public class DDRUMResourceEventRUMConnectivityCellular: NSObject @objc public var carrierName: String? @objc public var technology: String? +public enum DDRUMResourceEventRUMConnectivityEffectiveType: Int + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g public enum DDRUMResourceEventRUMConnectivityInterfaces: Int + case none case bluetooth case cellular case ethernet @@ -780,11 +980,24 @@ public enum DDRUMResourceEventRUMConnectivityInterfaces: Int case mixed case other case unknown - case none + case interfacesNone public enum DDRUMResourceEventRUMConnectivityStatus: Int case connected case notConnected case maybe +public class DDRUMResourceEventContainer: NSObject + @objc public var source: DDRUMResourceEventContainerSource + @objc public var view: DDRUMResourceEventContainerView +public enum DDRUMResourceEventContainerSource: Int + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMResourceEventContainerView: NSObject + @objc public var id: String public class DDRUMResourceEventRUMEventAttributes: NSObject @objc public var contextInfo: [String: Any] public class DDRUMResourceEventRUMDevice: NSObject @@ -817,6 +1030,7 @@ public class DDRUMResourceEventResource: NSObject @objc public var download: DDRUMResourceEventResourceDownload? @objc public var duration: NSNumber? @objc public var firstByte: DDRUMResourceEventResourceFirstByte? + @objc public var graphql: DDRUMResourceEventResourceGraphql? @objc public var id: String? @objc public var method: DDRUMResourceEventResourceRUMMethod @objc public var provider: DDRUMResourceEventResourceProvider? @@ -838,6 +1052,15 @@ public class DDRUMResourceEventResourceDownload: NSObject public class DDRUMResourceEventResourceFirstByte: NSObject @objc public var duration: NSNumber @objc public var start: NSNumber +public class DDRUMResourceEventResourceGraphql: NSObject + @objc public var operationName: String? + @objc public var operationType: DDRUMResourceEventResourceGraphqlOperationType + @objc public var payload: String? + @objc public var variables: String? +public enum DDRUMResourceEventResourceGraphqlOperationType: Int + case query + case mutation + case subscription public enum DDRUMResourceEventResourceRUMMethod: Int case none case post @@ -846,6 +1069,9 @@ public enum DDRUMResourceEventResourceRUMMethod: Int case put case delete case patch + case trace + case options + case connect public class DDRUMResourceEventResourceProvider: NSObject @objc public var domain: String? @objc public var name: String? @@ -887,8 +1113,8 @@ public enum DDRUMResourceEventResourceResourceType: Int public class DDRUMResourceEventSession: NSObject @objc public var hasReplay: NSNumber? @objc public var id: String - @objc public var type: DDRUMResourceEventSessionSessionType -public enum DDRUMResourceEventSessionSessionType: Int + @objc public var type: DDRUMResourceEventSessionRUMSessionType +public enum DDRUMResourceEventSessionRUMSessionType: Int case user case synthetics case ciTest @@ -900,7 +1126,8 @@ public enum DDRUMResourceEventSource: Int case flutter case reactNative case roku -public class DDRUMResourceEventSynthetics: NSObject + case unity +public class DDRUMResourceEventRUMSyntheticsTest: NSObject @objc public var injected: NSNumber? @objc public var resultId: String @objc public var testId: String @@ -917,8 +1144,11 @@ public class DDRUMResourceEventView: NSObject public class DDRUMViewEvent: NSObject @objc public var dd: DDRUMViewEventDD @objc public var application: DDRUMViewEventApplication + @objc public var buildId: String? + @objc public var buildVersion: String? @objc public var ciTest: DDRUMViewEventRUMCITest? @objc public var connectivity: DDRUMViewEventRUMConnectivity? + @objc public var container: DDRUMViewEventContainer? @objc public var context: DDRUMViewEventRUMEventAttributes? @objc public var date: NSNumber @objc public var device: DDRUMViewEventRUMDevice? @@ -929,18 +1159,23 @@ public class DDRUMViewEvent: NSObject @objc public var service: String? @objc public var session: DDRUMViewEventSession @objc public var source: DDRUMViewEventSource - @objc public var synthetics: DDRUMViewEventSynthetics? + @objc public var synthetics: DDRUMViewEventRUMSyntheticsTest? @objc public var type: String @objc public var usr: DDRUMViewEventRUMUser? @objc public var version: String? @objc public var view: DDRUMViewEventView public class DDRUMViewEventDD: NSObject @objc public var browserSdkVersion: String? + @objc public var configuration: DDRUMViewEventDDConfiguration? @objc public var documentVersion: NSNumber @objc public var formatVersion: NSNumber @objc public var pageStates: [DDRUMViewEventDDPageStates]? @objc public var replayStats: DDRUMViewEventDDReplayStats? @objc public var session: DDRUMViewEventDDSession? +public class DDRUMViewEventDDConfiguration: NSObject + @objc public var sessionReplaySampleRate: NSNumber? + @objc public var sessionSampleRate: NSNumber + @objc public var startSessionReplayRecordingManually: NSNumber? public class DDRUMViewEventDDPageStates: NSObject @objc public var start: NSNumber @objc public var state: DDRUMViewEventDDPageStatesState @@ -956,22 +1191,40 @@ public class DDRUMViewEventDDReplayStats: NSObject @objc public var segmentsTotalRawSize: NSNumber? public class DDRUMViewEventDDSession: NSObject @objc public var plan: DDRUMViewEventDDSessionPlan + @objc public var sessionPrecondition: DDRUMViewEventDDSessionRUMSessionPrecondition public enum DDRUMViewEventDDSessionPlan: Int case none case plan1 case plan2 +public enum DDRUMViewEventDDSessionRUMSessionPrecondition: Int + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop public class DDRUMViewEventApplication: NSObject @objc public var id: String public class DDRUMViewEventRUMCITest: NSObject @objc public var testExecutionId: String public class DDRUMViewEventRUMConnectivity: NSObject @objc public var cellular: DDRUMViewEventRUMConnectivityCellular? - @objc public var interfaces: [Int] + @objc public var effectiveType: DDRUMViewEventRUMConnectivityEffectiveType + @objc public var interfaces: [Int]? @objc public var status: DDRUMViewEventRUMConnectivityStatus public class DDRUMViewEventRUMConnectivityCellular: NSObject @objc public var carrierName: String? @objc public var technology: String? +public enum DDRUMViewEventRUMConnectivityEffectiveType: Int + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g public enum DDRUMViewEventRUMConnectivityInterfaces: Int + case none case bluetooth case cellular case ethernet @@ -980,11 +1233,24 @@ public enum DDRUMViewEventRUMConnectivityInterfaces: Int case mixed case other case unknown - case none + case interfacesNone public enum DDRUMViewEventRUMConnectivityStatus: Int case connected case notConnected case maybe +public class DDRUMViewEventContainer: NSObject + @objc public var source: DDRUMViewEventContainerSource + @objc public var view: DDRUMViewEventContainerView +public enum DDRUMViewEventContainerSource: Int + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMViewEventContainerView: NSObject + @objc public var id: String public class DDRUMViewEventRUMEventAttributes: NSObject @objc public var contextInfo: [String: Any] public class DDRUMViewEventRUMDevice: NSObject @@ -1006,9 +1272,9 @@ public class DDRUMViewEventDisplay: NSObject @objc public var viewport: DDRUMViewEventDisplayViewport? public class DDRUMViewEventDisplayScroll: NSObject @objc public var maxDepth: NSNumber - @objc public var maxDepthScrollHeight: NSNumber @objc public var maxDepthScrollTop: NSNumber - @objc public var maxDepthTime: NSNumber + @objc public var maxScrollHeight: NSNumber + @objc public var maxScrollHeightTime: NSNumber public class DDRUMViewEventDisplayViewport: NSObject @objc public var height: NSNumber @objc public var width: NSNumber @@ -1030,16 +1296,8 @@ public class DDRUMViewEventSession: NSObject @objc public var id: String @objc public var isActive: NSNumber? @objc public var sampledForReplay: NSNumber? - @objc public var startPrecondition: DDRUMViewEventSessionStartPrecondition - @objc public var type: DDRUMViewEventSessionSessionType -public enum DDRUMViewEventSessionStartPrecondition: Int - case none - case appLaunch - case inactivityTimeout - case maxDuration - case explicitStop - case backgroundEvent -public enum DDRUMViewEventSessionSessionType: Int + @objc public var type: DDRUMViewEventSessionRUMSessionType +public enum DDRUMViewEventSessionRUMSessionType: Int case user case synthetics case ciTest @@ -1051,7 +1309,8 @@ public enum DDRUMViewEventSource: Int case flutter case reactNative case roku -public class DDRUMViewEventSynthetics: NSObject + case unity +public class DDRUMViewEventRUMSyntheticsTest: NSObject @objc public var injected: NSNumber? @objc public var resultId: String @objc public var testId: String @@ -1066,6 +1325,7 @@ public class DDRUMViewEventView: NSObject @objc public var cpuTicksPerSecond: NSNumber? @objc public var crash: DDRUMViewEventViewCrash? @objc public var cumulativeLayoutShift: NSNumber? + @objc public var cumulativeLayoutShiftTargetSelector: String? @objc public var customTimings: [String: NSNumber]? @objc public var domComplete: NSNumber? @objc public var domContentLoaded: NSNumber? @@ -1074,6 +1334,7 @@ public class DDRUMViewEventView: NSObject @objc public var firstByte: NSNumber? @objc public var firstContentfulPaint: NSNumber? @objc public var firstInputDelay: NSNumber? + @objc public var firstInputTargetSelector: String? @objc public var firstInputTime: NSNumber? @objc public var flutterBuildTime: DDRUMViewEventViewFlutterBuildTime? @objc public var flutterRasterTime: DDRUMViewEventViewFlutterRasterTime? @@ -1081,10 +1342,13 @@ public class DDRUMViewEventView: NSObject @objc public var frustration: DDRUMViewEventViewFrustration? @objc public var id: String @objc public var inForegroundPeriods: [DDRUMViewEventViewInForegroundPeriods]? + @objc public var interactionToNextPaint: NSNumber? + @objc public var interactionToNextPaintTargetSelector: String? @objc public var isActive: NSNumber? @objc public var isSlowRendered: NSNumber? @objc public var jsRefreshRate: DDRUMViewEventViewJsRefreshRate? @objc public var largestContentfulPaint: NSNumber? + @objc public var largestContentfulPaintTargetSelector: String? @objc public var loadEvent: NSNumber? @objc public var loadingTime: NSNumber? @objc public var loadingType: DDRUMViewEventViewLoadingType @@ -1140,6 +1404,162 @@ public class DDRUMViewEventViewLongTask: NSObject @objc public var count: NSNumber public class DDRUMViewEventViewResource: NSObject @objc public var count: NSNumber +public class DDRUMVitalEvent: NSObject + @objc public var dd: DDRUMVitalEventDD + @objc public var application: DDRUMVitalEventApplication + @objc public var buildId: String? + @objc public var buildVersion: String? + @objc public var ciTest: DDRUMVitalEventRUMCITest? + @objc public var connectivity: DDRUMVitalEventRUMConnectivity? + @objc public var container: DDRUMVitalEventContainer? + @objc public var context: DDRUMVitalEventRUMEventAttributes? + @objc public var date: NSNumber + @objc public var device: DDRUMVitalEventRUMDevice? + @objc public var display: DDRUMVitalEventDisplay? + @objc public var os: DDRUMVitalEventRUMOperatingSystem? + @objc public var service: String? + @objc public var session: DDRUMVitalEventSession + @objc public var source: DDRUMVitalEventSource + @objc public var synthetics: DDRUMVitalEventRUMSyntheticsTest? + @objc public var type: String + @objc public var usr: DDRUMVitalEventRUMUser? + @objc public var version: String? + @objc public var view: DDRUMVitalEventView + @objc public var vital: DDRUMVitalEventVital +public class DDRUMVitalEventDD: NSObject + @objc public var browserSdkVersion: String? + @objc public var configuration: DDRUMVitalEventDDConfiguration? + @objc public var formatVersion: NSNumber + @objc public var session: DDRUMVitalEventDDSession? +public class DDRUMVitalEventDDConfiguration: NSObject + @objc public var sessionReplaySampleRate: NSNumber? + @objc public var sessionSampleRate: NSNumber +public class DDRUMVitalEventDDSession: NSObject + @objc public var plan: DDRUMVitalEventDDSessionPlan + @objc public var sessionPrecondition: DDRUMVitalEventDDSessionRUMSessionPrecondition +public enum DDRUMVitalEventDDSessionPlan: Int + case none + case plan1 + case plan2 +public enum DDRUMVitalEventDDSessionRUMSessionPrecondition: Int + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +public class DDRUMVitalEventApplication: NSObject + @objc public var id: String +public class DDRUMVitalEventRUMCITest: NSObject + @objc public var testExecutionId: String +public class DDRUMVitalEventRUMConnectivity: NSObject + @objc public var cellular: DDRUMVitalEventRUMConnectivityCellular? + @objc public var effectiveType: DDRUMVitalEventRUMConnectivityEffectiveType + @objc public var interfaces: [Int]? + @objc public var status: DDRUMVitalEventRUMConnectivityStatus +public class DDRUMVitalEventRUMConnectivityCellular: NSObject + @objc public var carrierName: String? + @objc public var technology: String? +public enum DDRUMVitalEventRUMConnectivityEffectiveType: Int + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +public enum DDRUMVitalEventRUMConnectivityInterfaces: Int + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +public enum DDRUMVitalEventRUMConnectivityStatus: Int + case connected + case notConnected + case maybe +public class DDRUMVitalEventContainer: NSObject + @objc public var source: DDRUMVitalEventContainerSource + @objc public var view: DDRUMVitalEventContainerView +public enum DDRUMVitalEventContainerSource: Int + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMVitalEventContainerView: NSObject + @objc public var id: String +public class DDRUMVitalEventRUMEventAttributes: NSObject + @objc public var contextInfo: [String: Any] +public class DDRUMVitalEventRUMDevice: NSObject + @objc public var architecture: String? + @objc public var brand: String? + @objc public var model: String? + @objc public var name: String? + @objc public var type: DDRUMVitalEventRUMDeviceRUMDeviceType +public enum DDRUMVitalEventRUMDeviceRUMDeviceType: Int + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +public class DDRUMVitalEventDisplay: NSObject + @objc public var viewport: DDRUMVitalEventDisplayViewport? +public class DDRUMVitalEventDisplayViewport: NSObject + @objc public var height: NSNumber + @objc public var width: NSNumber +public class DDRUMVitalEventRUMOperatingSystem: NSObject + @objc public var build: String? + @objc public var name: String + @objc public var version: String + @objc public var versionMajor: String +public class DDRUMVitalEventSession: NSObject + @objc public var hasReplay: NSNumber? + @objc public var id: String + @objc public var type: DDRUMVitalEventSessionRUMSessionType +public enum DDRUMVitalEventSessionRUMSessionType: Int + case user + case synthetics + case ciTest +public enum DDRUMVitalEventSource: Int + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity +public class DDRUMVitalEventRUMSyntheticsTest: NSObject + @objc public var injected: NSNumber? + @objc public var resultId: String + @objc public var testId: String +public class DDRUMVitalEventRUMUser: NSObject + @objc public var email: String? + @objc public var id: String? + @objc public var name: String? + @objc public var usrInfo: [String: Any] +public class DDRUMVitalEventView: NSObject + @objc public var id: String + @objc public var name: String? + @objc public var referrer: String? + @objc public var url: String +public class DDRUMVitalEventVital: NSObject + @objc public var custom: [String: NSNumber]? + @objc public var id: String + @objc public var name: String? + @objc public var type: DDRUMVitalEventVitalVitalType +public enum DDRUMVitalEventVitalVitalType: Int + case duration public class DDTelemetryErrorEvent: NSObject @objc public var dd: DDTelemetryErrorEventDD @objc public var action: DDTelemetryErrorEventAction? @@ -1167,6 +1587,7 @@ public enum DDTelemetryErrorEventSource: Int case browser case flutter case reactNative + case unity public class DDTelemetryErrorEventTelemetry: NSObject @objc public var error: DDTelemetryErrorEventTelemetryError? @objc public var message: String @@ -1204,10 +1625,12 @@ public enum DDTelemetryDebugEventSource: Int case browser case flutter case reactNative + case unity public class DDTelemetryDebugEventTelemetry: NSObject @objc public var message: String @objc public var status: String @objc public var type: String? + @objc public var telemetryInfo: [String: Any] public class DDTelemetryDebugEventView: NSObject @objc public var id: String public class DDTelemetryConfigurationEvent: NSObject @@ -1237,11 +1660,17 @@ public enum DDTelemetryConfigurationEventSource: Int case browser case flutter case reactNative + case unity public class DDTelemetryConfigurationEventTelemetry: NSObject @objc public var configuration: DDTelemetryConfigurationEventTelemetryConfiguration @objc public var type: String public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject @objc public var actionNameAttribute: String? + @objc public var allowFallbackToLocalStorage: NSNumber? + @objc public var allowUntrustedEvents: NSNumber? + @objc public var appHangThreshold: NSNumber? + @objc public var backgroundTasksEnabled: NSNumber? + @objc public var batchProcessingLevel: NSNumber? @objc public var batchSize: NSNumber? @objc public var batchUploadFrequency: NSNumber? @objc public var dartVersion: String? @@ -1260,6 +1689,7 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject @objc public var sessionSampleRate: NSNumber? @objc public var silentMultipleInit: NSNumber? @objc public var startSessionReplayRecordingManually: NSNumber? + @objc public var storeContextsAcrossPages: NSNumber? @objc public var telemetryConfigurationSampleRate: NSNumber? @objc public var telemetrySampleRate: NSNumber? @objc public var traceSampleRate: NSNumber? @@ -1278,6 +1708,7 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject @objc public var trackSessionAcrossSubdomains: NSNumber? @objc public var trackUserInteractions: NSNumber? @objc public var trackViewsManually: NSNumber? + @objc public var unityVersion: String? @objc public var useAllowedTracingOrigins: NSNumber? @objc public var useAllowedTracingUrls: NSNumber? @objc public var useBeforeSend: NSNumber? @@ -1285,9 +1716,11 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject @objc public var useExcludedActivityUrls: NSNumber? @objc public var useFirstPartyHosts: NSNumber? @objc public var useLocalEncryption: NSNumber? + @objc public var usePartitionedCrossSiteSessionCookie: NSNumber? @objc public var useProxy: NSNumber? @objc public var useSecureSessionCookie: NSNumber? @objc public var useTracing: NSNumber? + @objc public var useWorkerUrl: NSNumber? @objc public var viewTrackingStrategy: DDTelemetryConfigurationEventTelemetryConfigurationViewTrackingStrategy public class DDTelemetryConfigurationEventTelemetryConfigurationForwardConsoleLogs: NSObject @objc public var stringsArray: [String]? @@ -1309,6 +1742,39 @@ public enum DDTelemetryConfigurationEventTelemetryConfigurationViewTrackingStrat case navigationViewTrackingStrategy public class DDTelemetryConfigurationEventView: NSObject @objc public var id: String +public final class DDSessionReplay: NSObject + public static func enable(with configuration: DDSessionReplayConfiguration) +public final class DDSessionReplayConfiguration: NSObject + @objc public var replaySampleRate: Float + @objc public var defaultPrivacyLevel: DDSessionReplayConfigurationPrivacyLevel + @objc public var customEndpoint: URL? + public required init(replaySampleRate: Float) +public enum DDSessionReplayConfigurationPrivacyLevel: Int + case allow + case mask + case maskUserInput +public enum DDInjectEncoding: Int + case multiple = 0 + case single = 1 +public class DDOTelHTTPHeadersWriter: DDB3HTTPHeadersWriter +public class DDB3HTTPHeadersWriter: NSObject + @objc public var traceHeaderFields: [String: String] + public convenience init(samplingRate: Float,injectEncoding: DDInjectEncoding = .single) + public init(sampleRate: Float = 20,injectEncoding: DDInjectEncoding = .single) + public init(samplingStrategy: DDTraceSamplingStrategy,injectEncoding: DDInjectEncoding = .single) +public class DDHTTPHeadersWriter: NSObject + @objc public var traceHeaderFields: [String: String] + public convenience init(samplingRate: Float) + public init(sampleRate: Float = 20) + public init(samplingStrategy: DDTraceSamplingStrategy) +public class DDTraceSamplingStrategy: NSObject + public static func headBased() -> DDTraceSamplingStrategy + public static func custom(sampleRate: Float) -> DDTraceSamplingStrategy +public class DDW3CHTTPHeadersWriter: NSObject + @objc public var traceHeaderFields: [String: String] + public convenience init(samplingRate: Float) + public init(sampleRate: Float = 20) + public init(samplingStrategy: DDTraceSamplingStrategy) public class DDTraceConfiguration: NSObject override public init() @objc public var sampleRate: Float @@ -1337,15 +1803,3 @@ public class DDTracer: NSObject, DatadogObjc.OTTracer public func startSpan(_ operationName: String,childOf parent: OTSpanContext?,tags: NSDictionary?,startTime: Date?) -> OTSpan public func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws public func extractWithFormat(_ format: String, carrier: Any) throws -public class DDHTTPHeadersWriter: NSObject - @objc public var traceHeaderFields: [String: String] - public init(samplingRate: Float = 20) -public enum DDInjectEncoding: Int - case multiple = 0 - case single = 1 -public class DDOTelHTTPHeadersWriter: NSObject - @objc public var traceHeaderFields: [String: String] - public init(samplingRate: Float = 20,injectEncoding: DDInjectEncoding = .single) -public class DDW3CHTTPHeadersWriter: NSObject - @objc public var traceHeaderFields: [String: String] - public init(samplingRate: Float = 20) diff --git a/api-surface-swift b/api-surface-swift index 8ad9b41d00..33f2c8afa7 100644 --- a/api-surface-swift +++ b/api-surface-swift @@ -16,7 +16,7 @@ public struct _TelemetryProxy public func error(id: String, message: String, kind: String?, stack: String?) [?] extension InternalExtension where ExtendedType == Datadog.Configuration public var additionalConfiguration: [String: Any] -public struct Datadog +public enum Datadog public struct Configuration public enum BatchSize case small @@ -26,6 +26,10 @@ public struct Datadog case frequent case average case rare + public enum BatchProcessingLevel + case low + case medium + case high public var clientToken: String public var env: String public var site: DatadogSite @@ -36,14 +40,17 @@ public struct Datadog public var encryption: DataEncryption? public var serverDateProvider: ServerDateProvider public var bundle: Bundle - public init(clientToken: String,env: String,site: DatadogSite = .us1,service: String? = nil,bundle: Bundle = .main,batchSize: BatchSize = .medium,uploadFrequency: UploadFrequency = .average,proxyConfiguration: [AnyHashable: Any]? = nil,encryption: DataEncryption? = nil,serverDateProvider: ServerDateProvider? = nil) - public static var verbosityLevel: CoreLoggerLevel? = nil + public var batchProcessingLevel: BatchProcessingLevel + public var backgroundTasksEnabled: Bool + public init(clientToken: String,env: String,site: DatadogSite = .us1,service: String? = nil,bundle: Bundle = .main,batchSize: BatchSize = .medium,uploadFrequency: UploadFrequency = .average,proxyConfiguration: [AnyHashable: Any]? = nil,encryption: DataEncryption? = nil,serverDateProvider: ServerDateProvider? = nil,batchProcessingLevel: BatchProcessingLevel = .medium,backgroundTasksEnabled: Bool = false) + public static var verbosityLevel: CoreLoggerLevel? public static func isInitialized(instanceName name: String = CoreRegistry.defaultInstanceName) -> Bool public static func sdkInstance(named name: String) -> DatadogCoreProtocol public static func setUserInfo(id: String? = nil,name: String? = nil,email: String? = nil,extraInfo: [AttributeKey: AttributeValue] = [:],in core: DatadogCoreProtocol = CoreRegistry.default) public static func addUserExtraInfo(_ extraInfo: [AttributeKey: AttributeValue?],in core: DatadogCoreProtocol = CoreRegistry.default) public static func set(trackingConsent: TrackingConsent, in core: DatadogCoreProtocol = CoreRegistry.default) public static func clearAllData(in core: DatadogCoreProtocol = CoreRegistry.default) + public static func stopInstance(named instanceName: String = CoreRegistry.defaultInstanceName) public static func initialize(with configuration: Configuration,trackingConsent: TrackingConsent,instanceName: String = CoreRegistry.defaultInstanceName) -> DatadogCoreProtocol @@ -71,8 +78,17 @@ public struct LogEvent: Encodable public var kind: String? public var message: String? public var stack: String? + public var sourceType: String = "ios" + public var fingerprint: String? public struct DeviceInfo: Codable + public let brand: String + public let name: String + public let model: String public let architecture: String + public struct OperatingSystem: Codable + public let name: String + public let version: String + public let build: String? public struct Dd: Codable public let device: DeviceInfo public let date: Date @@ -85,7 +101,11 @@ public struct LogEvent: Encodable public let loggerVersion: String public let threadName: String? public let applicationVersion: String + public let applicationBuildNumber: String + public let buildId: String? + public let variant: String? public let dd: Dd + public let os: OperatingSystem public var userInfo: UserInfo public let networkConnectionInfo: NetworkConnectionInfo? public let mobileCarrierInfo: CarrierInfo? @@ -118,6 +138,8 @@ public enum LogLevel: Int, Codable case warn case error case critical +[?] extension CoreLoggerLevel + public init(logLevel: LogLevel) public protocol LoggerProtocol func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) func addAttribute(forKey key: AttributeKey, value: AttributeValue) @@ -133,13 +155,20 @@ public extension LoggerProtocol func warn(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) func error(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) func critical(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) -public struct Logs +[?] extension InternalExtension where ExtendedType == Logs + public static func isEnabled(in core: DatadogCoreProtocol = CoreRegistry.default) -> Bool +public enum Logs public struct Configuration public typealias EventMapper = (LogEvent) -> LogEvent? public var eventMapper: EventMapper? public var customEndpoint: URL? public init(eventMapper: EventMapper? = nil,customEndpoint: URL? = nil) public static func enable(with configuration: Configuration = .init(),in core: DatadogCoreProtocol = CoreRegistry.default) + public static func addAttribute(forKey key: AttributeKey, value: AttributeValue, in core: DatadogCoreProtocol = CoreRegistry.default) + public static func removeAttribute(forKey key: AttributeKey, in core: DatadogCoreProtocol = CoreRegistry.default) +[?] extension Logs + public struct Attributes + public static let errorFingerprint = "_dd.error.fingerprint" [?] extension InternalExtension where ExtendedType == Logs.Configuration public mutating func setLogEventMapper(_ mapper: LogEventMapper) public protocol LogEventMapper @@ -250,7 +279,7 @@ public struct SpanEvent: Encodable public var userInfo: UserInfo public var tags: [String: String] public func encode(to encoder: Encoder) throws -public struct Trace +public enum Trace public static func enable(with configuration: Trace.Configuration = .init(), in core: DatadogCoreProtocol = CoreRegistry.default) [?] extension Trace public struct Configuration @@ -284,8 +313,11 @@ public struct RUMActionEvent: RUMDataModel public let dd: DD public var action: Action public let application: Application + public let buildId: String? + public let buildVersion: String? public let ciTest: RUMCITest? public let connectivity: RUMConnectivity? + public let container: Container? public internal(set) var context: RUMEventAttributes? public let date: Int64 public let device: RUMDevice? @@ -294,7 +326,7 @@ public struct RUMActionEvent: RUMDataModel public let service: String? public let session: Session public let source: Source? - public let synthetics: Synthetics? + public let synthetics: RUMSyntheticsTest? public let type: String = "action" public internal(set) var usr: RUMUser? public let version: String? @@ -302,6 +334,7 @@ public struct RUMActionEvent: RUMDataModel public struct DD: Codable public let action: Action? public let browserSdkVersion: String? + public let configuration: Configuration? public let formatVersion: Int64 = 2 public let session: Session? public struct Action: Codable @@ -314,8 +347,12 @@ public struct RUMActionEvent: RUMDataModel public let height: Int64? public let selector: String? public let width: Int64? + public struct Configuration: Codable + public let sessionReplaySampleRate: Double? + public let sessionSampleRate: Double public struct Session: Codable public let plan: Plan? + public let sessionPrecondition: RUMSessionPrecondition? public enum Plan: Int, Codable case plan1 = 1 case plan2 = 2 @@ -357,6 +394,19 @@ public struct RUMActionEvent: RUMDataModel case back = "back" public struct Application: Codable public let id: String + public struct Container: Codable + public let source: Source + public let view: View + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String public struct Display: Codable public let viewport: Viewport? public struct Viewport: Codable @@ -365,11 +415,7 @@ public struct RUMActionEvent: RUMDataModel public struct Session: Codable public let hasReplay: Bool? public let id: String - public let type: SessionType - public enum SessionType: String, Codable - case user = "user" - case synthetics = "synthetics" - case ciTest = "ci_test" + public let type: RUMSessionType public enum Source: String, Codable case android = "android" case ios = "ios" @@ -377,10 +423,7 @@ public struct RUMActionEvent: RUMDataModel case flutter = "flutter" case reactNative = "react-native" case roku = "roku" - public struct Synthetics: Codable - public let injected: Bool? - public let resultId: String - public let testId: String + case unity = "unity" public struct View: Codable public let id: String public let inForeground: Bool? @@ -391,29 +434,38 @@ public struct RUMErrorEvent: RUMDataModel public let dd: DD public let action: Action? public let application: Application + public let buildId: String? + public let buildVersion: String? public let ciTest: RUMCITest? public let connectivity: RUMConnectivity? + public let container: Container? public internal(set) var context: RUMEventAttributes? public let date: Int64 public let device: RUMDevice? public let display: Display? public var error: Error public internal(set) var featureFlags: FeatureFlags? + public let freeze: Freeze? public let os: RUMOperatingSystem? public let service: String? public let session: Session public let source: Source? - public let synthetics: Synthetics? + public let synthetics: RUMSyntheticsTest? public let type: String = "error" public internal(set) var usr: RUMUser? public let version: String? public var view: View public struct DD: Codable public let browserSdkVersion: String? + public let configuration: Configuration? public let formatVersion: Int64 = 2 public let session: Session? + public struct Configuration: Codable + public let sessionReplaySampleRate: Double? + public let sessionSampleRate: Double public struct Session: Codable public let plan: Plan? + public let sessionPrecondition: RUMSessionPrecondition? public enum Plan: Int, Codable case plan1 = 1 case plan2 = 2 @@ -421,12 +473,27 @@ public struct RUMErrorEvent: RUMDataModel public let id: RUMActionID public struct Application: Codable public let id: String + public struct Container: Codable + public let source: Source + public let view: View + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String public struct Display: Codable public let viewport: Viewport? public struct Viewport: Codable public let height: Double public let width: Double public struct Error: Codable + public let binaryImages: [BinaryImages]? + public let category: Category? public var causes: [Causes]? public var fingerprint: String? public let handling: Handling? @@ -434,11 +501,25 @@ public struct RUMErrorEvent: RUMDataModel public let id: String? public let isCrash: Bool? public var message: String + public let meta: Meta? public var resource: Resource? public let source: Source public let sourceType: SourceType? public var stack: String? + public let threads: [Threads]? public let type: String? + public let wasTruncated: Bool? + public struct BinaryImages: Codable + public let arch: String? + public let isSystem: Bool + public let loadAddress: String? + public let maxAddress: String? + public let name: String + public let uuid: String + public enum Category: String, Codable + case aNR = "ANR" + case appHang = "App Hang" + case exception = "Exception" public struct Causes: Codable public var message: String public let source: Source @@ -456,6 +537,14 @@ public struct RUMErrorEvent: RUMDataModel public enum Handling: String, Codable case handled = "handled" case unhandled = "unhandled" + public struct Meta: Codable + public let codeType: String? + public let exceptionCodes: String? + public let exceptionType: String? + public let incidentIdentifier: String? + public let parentProcess: String? + public let path: String? + public let process: String? public struct Resource: Codable public let method: RUMMethod public let provider: Provider? @@ -496,16 +585,22 @@ public struct RUMErrorEvent: RUMDataModel case reactNative = "react-native" case flutter = "flutter" case roku = "roku" + case ndk = "ndk" + case iosIl2cpp = "ios+il2cpp" + case ndkIl2cpp = "ndk+il2cpp" + public struct Threads: Codable + public let crashed: Bool + public let name: String + public let stack: String + public let state: String? public struct FeatureFlags: Codable public internal(set) var featureFlagsInfo: [String: Encodable] + public struct Freeze: Codable + public let duration: Int64 public struct Session: Codable public let hasReplay: Bool? public let id: String - public let type: SessionType - public enum SessionType: String, Codable - case user = "user" - case synthetics = "synthetics" - case ciTest = "ci_test" + public let type: RUMSessionType public enum Source: String, Codable case android = "android" case ios = "ios" @@ -513,10 +608,7 @@ public struct RUMErrorEvent: RUMDataModel case flutter = "flutter" case reactNative = "react-native" case roku = "roku" - public struct Synthetics: Codable - public let injected: Bool? - public let resultId: String - public let testId: String + case unity = "unity" public struct View: Codable public let id: String public let inForeground: Bool? @@ -530,8 +622,11 @@ public struct RUMLongTaskEvent: RUMDataModel public let dd: DD public let action: Action? public let application: Application + public let buildId: String? + public let buildVersion: String? public let ciTest: RUMCITest? public let connectivity: RUMConnectivity? + public let container: Container? public internal(set) var context: RUMEventAttributes? public let date: Int64 public let device: RUMDevice? @@ -541,18 +636,23 @@ public struct RUMLongTaskEvent: RUMDataModel public let service: String? public let session: Session public let source: Source? - public let synthetics: Synthetics? + public let synthetics: RUMSyntheticsTest? public let type: String = "long_task" public internal(set) var usr: RUMUser? public let version: String? public var view: View public struct DD: Codable public let browserSdkVersion: String? + public let configuration: Configuration? public let discarded: Bool? public let formatVersion: Int64 = 2 public let session: Session? + public struct Configuration: Codable + public let sessionReplaySampleRate: Double? + public let sessionSampleRate: Double public struct Session: Codable public let plan: Plan? + public let sessionPrecondition: RUMSessionPrecondition? public enum Plan: Int, Codable case plan1 = 1 case plan2 = 2 @@ -560,6 +660,19 @@ public struct RUMLongTaskEvent: RUMDataModel public let id: RUMActionID public struct Application: Codable public let id: String + public struct Container: Codable + public let source: Source + public let view: View + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String public struct Display: Codable public let viewport: Viewport? public struct Viewport: Codable @@ -572,11 +685,7 @@ public struct RUMLongTaskEvent: RUMDataModel public struct Session: Codable public let hasReplay: Bool? public let id: String - public let type: SessionType - public enum SessionType: String, Codable - case user = "user" - case synthetics = "synthetics" - case ciTest = "ci_test" + public let type: RUMSessionType public enum Source: String, Codable case android = "android" case ios = "ios" @@ -584,10 +693,7 @@ public struct RUMLongTaskEvent: RUMDataModel case flutter = "flutter" case reactNative = "react-native" case roku = "roku" - public struct Synthetics: Codable - public let injected: Bool? - public let resultId: String - public let testId: String + case unity = "unity" public struct View: Codable public let id: String public var name: String? @@ -597,8 +703,11 @@ public struct RUMResourceEvent: RUMDataModel public let dd: DD public let action: Action? public let application: Application + public let buildId: String? + public let buildVersion: String? public let ciTest: RUMCITest? public let connectivity: RUMConnectivity? + public let container: Container? public internal(set) var context: RUMEventAttributes? public let date: Int64 public let device: RUMDevice? @@ -608,21 +717,26 @@ public struct RUMResourceEvent: RUMDataModel public let service: String? public let session: Session public let source: Source? - public let synthetics: Synthetics? + public let synthetics: RUMSyntheticsTest? public let type: String = "resource" public internal(set) var usr: RUMUser? public let version: String? public var view: View public struct DD: Codable public let browserSdkVersion: String? + public let configuration: Configuration? public let discarded: Bool? public let formatVersion: Int64 = 2 public let rulePsr: Double? public let session: Session? public let spanId: String? public let traceId: String? + public struct Configuration: Codable + public let sessionReplaySampleRate: Double? + public let sessionSampleRate: Double public struct Session: Codable public let plan: Plan? + public let sessionPrecondition: RUMSessionPrecondition? public enum Plan: Int, Codable case plan1 = 1 case plan2 = 2 @@ -630,6 +744,19 @@ public struct RUMResourceEvent: RUMDataModel public let id: RUMActionID public struct Application: Codable public let id: String + public struct Container: Codable + public let source: Source + public let view: View + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String public struct Display: Codable public let viewport: Viewport? public struct Viewport: Codable @@ -641,6 +768,7 @@ public struct RUMResourceEvent: RUMDataModel public let download: Download? public let duration: Int64? public let firstByte: FirstByte? + public var graphql: Graphql? public let id: String? public let method: RUMMethod? public let provider: Provider? @@ -662,6 +790,15 @@ public struct RUMResourceEvent: RUMDataModel public struct FirstByte: Codable public let duration: Int64 public let start: Int64 + public struct Graphql: Codable + public let operationName: String? + public let operationType: OperationType + public var payload: String? + public var variables: String? + public enum OperationType: String, Codable + case query = "query" + case mutation = "mutation" + case subscription = "subscription" public struct Provider: Codable public let domain: String? public let name: String? @@ -702,11 +839,7 @@ public struct RUMResourceEvent: RUMDataModel public struct Session: Codable public let hasReplay: Bool? public let id: String - public let type: SessionType - public enum SessionType: String, Codable - case user = "user" - case synthetics = "synthetics" - case ciTest = "ci_test" + public let type: RUMSessionType public enum Source: String, Codable case android = "android" case ios = "ios" @@ -714,10 +847,7 @@ public struct RUMResourceEvent: RUMDataModel case flutter = "flutter" case reactNative = "react-native" case roku = "roku" - public struct Synthetics: Codable - public let injected: Bool? - public let resultId: String - public let testId: String + case unity = "unity" public struct View: Codable public let id: String public var name: String? @@ -726,8 +856,11 @@ public struct RUMResourceEvent: RUMDataModel public struct RUMViewEvent: RUMDataModel public let dd: DD public let application: Application + public let buildId: String? + public let buildVersion: String? public let ciTest: RUMCITest? public let connectivity: RUMConnectivity? + public let container: Container? public internal(set) var context: RUMEventAttributes? public let date: Int64 public let device: RUMDevice? @@ -738,18 +871,23 @@ public struct RUMViewEvent: RUMDataModel public let service: String? public let session: Session public let source: Source? - public let synthetics: Synthetics? + public let synthetics: RUMSyntheticsTest? public let type: String = "view" public internal(set) var usr: RUMUser? public let version: String? public var view: View public struct DD: Codable public let browserSdkVersion: String? + public let configuration: Configuration? public let documentVersion: Int64 public let formatVersion: Int64 = 2 public let pageStates: [PageStates]? public let replayStats: ReplayStats? public let session: Session? + public struct Configuration: Codable + public let sessionReplaySampleRate: Double? + public let sessionSampleRate: Double + public let startSessionReplayRecordingManually: Bool? public struct PageStates: Codable public let start: Int64 public let state: State @@ -765,19 +903,33 @@ public struct RUMViewEvent: RUMDataModel public let segmentsTotalRawSize: Int64? public struct Session: Codable public let plan: Plan? + public let sessionPrecondition: RUMSessionPrecondition? public enum Plan: Int, Codable case plan1 = 1 case plan2 = 2 public struct Application: Codable public let id: String + public struct Container: Codable + public let source: Source + public let view: View + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String public struct Display: Codable public let scroll: Scroll? public let viewport: Viewport? public struct Scroll: Codable public let maxDepth: Double - public let maxDepthScrollHeight: Double public let maxDepthScrollTop: Double - public let maxDepthTime: Double + public let maxScrollHeight: Double + public let maxScrollHeightTime: Double public struct Viewport: Codable public let height: Double public let width: Double @@ -794,18 +946,7 @@ public struct RUMViewEvent: RUMDataModel public let id: String public let isActive: Bool? public let sampledForReplay: Bool? - public let startPrecondition: StartPrecondition? - public let type: SessionType - public enum StartPrecondition: String, Codable - case appLaunch = "app_launch" - case inactivityTimeout = "inactivity_timeout" - case maxDuration = "max_duration" - case explicitStop = "explicit_stop" - case backgroundEvent = "background_event" - public enum SessionType: String, Codable - case user = "user" - case synthetics = "synthetics" - case ciTest = "ci_test" + public let type: RUMSessionType public enum Source: String, Codable case android = "android" case ios = "ios" @@ -813,16 +954,14 @@ public struct RUMViewEvent: RUMDataModel case flutter = "flutter" case reactNative = "react-native" case roku = "roku" - public struct Synthetics: Codable - public let injected: Bool? - public let resultId: String - public let testId: String + case unity = "unity" public struct View: Codable public let action: Action public let cpuTicksCount: Double? public let cpuTicksPerSecond: Double? public let crash: Crash? public let cumulativeLayoutShift: Double? + public let cumulativeLayoutShiftTargetSelector: String? public let customTimings: [String: Int64]? public let domComplete: Int64? public let domContentLoaded: Int64? @@ -831,6 +970,7 @@ public struct RUMViewEvent: RUMDataModel public let firstByte: Int64? public let firstContentfulPaint: Int64? public let firstInputDelay: Int64? + public let firstInputTargetSelector: String? public let firstInputTime: Int64? public let flutterBuildTime: FlutterBuildTime? public let flutterRasterTime: FlutterRasterTime? @@ -838,10 +978,13 @@ public struct RUMViewEvent: RUMDataModel public let frustration: Frustration? public let id: String public let inForegroundPeriods: [InForegroundPeriods]? + public let interactionToNextPaint: Int64? + public let interactionToNextPaintTargetSelector: String? public let isActive: Bool? public let isSlowRendered: Bool? public let jsRefreshRate: JsRefreshRate? public let largestContentfulPaint: Int64? + public let largestContentfulPaintTargetSelector: String? public let loadEvent: Int64? public let loadingTime: Int64? public let loadingType: LoadingType? @@ -899,6 +1042,86 @@ public struct RUMViewEvent: RUMDataModel [?] extension RUMViewEvent.FeatureFlags public func encode(to encoder: Encoder) throws public init(from decoder: Decoder) throws +public struct RUMVitalEvent: RUMDataModel + public let dd: DD + public let application: Application + public let buildId: String? + public let buildVersion: String? + public let ciTest: RUMCITest? + public let connectivity: RUMConnectivity? + public let container: Container? + public internal(set) var context: RUMEventAttributes? + public let date: Int64 + public let device: RUMDevice? + public let display: Display? + public let os: RUMOperatingSystem? + public let service: String? + public let session: Session + public let source: Source? + public let synthetics: RUMSyntheticsTest? + public let type: String = "vital" + public internal(set) var usr: RUMUser? + public let version: String? + public var view: View + public let vital: Vital + public struct DD: Codable + public let browserSdkVersion: String? + public let configuration: Configuration? + public let formatVersion: Int64 = 2 + public let session: Session? + public struct Configuration: Codable + public let sessionReplaySampleRate: Double? + public let sessionSampleRate: Double + public struct Session: Codable + public let plan: Plan? + public let sessionPrecondition: RUMSessionPrecondition? + public enum Plan: Int, Codable + case plan1 = 1 + case plan2 = 2 + public struct Application: Codable + public let id: String + public struct Container: Codable + public let source: Source + public let view: View + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String + public struct Display: Codable + public let viewport: Viewport? + public struct Viewport: Codable + public let height: Double + public let width: Double + public struct Session: Codable + public let hasReplay: Bool? + public let id: String + public let type: RUMSessionType + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + public struct View: Codable + public let id: String + public var name: String? + public var referrer: String? + public var url: String + public struct Vital: Codable + public let custom: [String: Double]? + public let id: String + public let name: String? + public let type: VitalType + public enum VitalType: String, Codable + case duration = "duration" public struct TelemetryErrorEvent: RUMDataModel public let dd: DD public let action: Action? @@ -926,6 +1149,7 @@ public struct TelemetryErrorEvent: RUMDataModel case browser = "browser" case flutter = "flutter" case reactNative = "react-native" + case unity = "unity" public struct Telemetry: Codable public let error: Error? public let message: String @@ -945,7 +1169,7 @@ public struct TelemetryDebugEvent: RUMDataModel public let service: String public let session: Session? public let source: Source - public let telemetry: Telemetry + public internal(set) var telemetry: Telemetry public let type: String = "telemetry" public let version: String public let view: View? @@ -963,12 +1187,17 @@ public struct TelemetryDebugEvent: RUMDataModel case browser = "browser" case flutter = "flutter" case reactNative = "react-native" + case unity = "unity" public struct Telemetry: Codable public let message: String public let status: String = "debug" public let type: String? = "log" + public internal(set) var telemetryInfo: [String: Encodable] public struct View: Codable public let id: String +[?] extension TelemetryDebugEvent.Telemetry + public func encode(to encoder: Encoder) throws + public init(from decoder: Decoder) throws public struct TelemetryConfigurationEvent: RUMDataModel public let dd: DD public let action: Action? @@ -996,11 +1225,17 @@ public struct TelemetryConfigurationEvent: RUMDataModel case browser = "browser" case flutter = "flutter" case reactNative = "react-native" + case unity = "unity" public struct Telemetry: Codable public var configuration: Configuration public let type: String = "configuration" public struct Configuration: Codable public let actionNameAttribute: String? + public let allowFallbackToLocalStorage: Bool? + public let allowUntrustedEvents: Bool? + public let appHangThreshold: Int64? + public let backgroundTasksEnabled: Bool? + public let batchProcessingLevel: Int64? public let batchSize: Int64? public let batchUploadFrequency: Int64? public var dartVersion: String? @@ -1019,6 +1254,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel public let sessionSampleRate: Int64? public let silentMultipleInit: Bool? public var startSessionReplayRecordingManually: Bool? + public let storeContextsAcrossPages: Bool? public let telemetryConfigurationSampleRate: Int64? public let telemetrySampleRate: Int64? public let traceSampleRate: Int64? @@ -1037,6 +1273,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel public let trackSessionAcrossSubdomains: Bool? public var trackUserInteractions: Bool? public var trackViewsManually: Bool? + public var unityVersion: String? public let useAllowedTracingOrigins: Bool? public let useAllowedTracingUrls: Bool? public let useBeforeSend: Bool? @@ -1044,9 +1281,11 @@ public struct TelemetryConfigurationEvent: RUMDataModel public let useExcludedActivityUrls: Bool? public var useFirstPartyHosts: Bool? public let useLocalEncryption: Bool? + public let usePartitionedCrossSiteSessionCookie: Bool? public var useProxy: Bool? public let useSecureSessionCookie: Bool? public let useTracing: Bool? + public let useWorkerUrl: Bool? public let viewTrackingStrategy: ViewTrackingStrategy? public enum ForwardConsoleLogs: Codable case stringsArray(value: [String]) @@ -1070,15 +1309,29 @@ public struct TelemetryConfigurationEvent: RUMDataModel case navigationViewTrackingStrategy = "NavigationViewTrackingStrategy" public struct View: Codable public let id: String +public enum RUMSessionPrecondition: String, Codable + case userAppLaunch = "user_app_launch" + case inactivityTimeout = "inactivity_timeout" + case maxDuration = "max_duration" + case backgroundLaunch = "background_launch" + case prewarm = "prewarm" + case fromNonInteractiveSession = "from_non_interactive_session" + case explicitStop = "explicit_stop" public struct RUMCITest: Codable public let testExecutionId: String public struct RUMConnectivity: Codable public let cellular: Cellular? - public let interfaces: [Interfaces] + public let effectiveType: EffectiveType? + public let interfaces: [Interfaces]? public let status: Status public struct Cellular: Codable public let carrierName: String? public let technology: String? + public enum EffectiveType: String, Codable + case slow2g = "slow_2g" + case effectiveType2g = "2g" + case effectiveType3g = "3g" + case effectiveType4g = "4g" public enum Interfaces: String, Codable case bluetooth = "bluetooth" case cellular = "cellular" @@ -1088,7 +1341,7 @@ public struct RUMConnectivity: Codable case mixed = "mixed" case other = "other" case unknown = "unknown" - case none = "none" + case interfacesNone = "none" public enum Status: String, Codable case connected = "connected" case notConnected = "not_connected" @@ -1117,6 +1370,14 @@ public struct RUMOperatingSystem: Codable public let name: String public let version: String public let versionMajor: String +public enum RUMSessionType: String, Codable + case user = "user" + case synthetics = "synthetics" + case ciTest = "ci_test" +public struct RUMSyntheticsTest: Codable + public let injected: Bool? + public let resultId: String + public let testId: String public struct RUMUser: Codable public let email: String? public let id: String? @@ -1137,6 +1398,9 @@ public enum RUMMethod: String, Codable case put = "PUT" case delete = "DELETE" case patch = "PATCH" + case trace = "TRACE" + case options = "OPTIONS" + case connect = "CONNECT" public extension SwiftUI.View func trackRUMTapAction(name: String,attributes: [String: Encodable] = [:],count: Int = 1,in core: DatadogCoreProtocol = CoreRegistry.default) -> some View public struct RUMAction @@ -1168,8 +1432,14 @@ public protocol UIKitRUMViewsPredicate public struct DefaultUIKitRUMViewsPredicate: UIKitRUMViewsPredicate public init () public func rumView(for viewController: UIViewController) -> RUMView? -public struct RUM +[?] extension InternalExtension where ExtendedType == RUM + public static func isEnabled(in core: DatadogCoreProtocol = CoreRegistry.default) -> Bool + public static func enableURLSessionTracking(with configuration: RUM.Configuration.URLSessionTracking,in core: DatadogCoreProtocol = CoreRegistry.default) throws +public enum RUM public static func enable(with configuration: RUM.Configuration, in core: DatadogCoreProtocol = CoreRegistry.default) +[?] extension RUM + public struct Attributes + public static let errorFingerprint = "_dd.error.fingerprint" [?] extension RUM public typealias ViewEventMapper = (RUMViewEvent) -> RUMViewEvent public typealias ResourceEventMapper = (RUMResourceEvent) -> RUMResourceEvent? @@ -1187,6 +1457,7 @@ public struct RUM public var trackFrustrations: Bool public var trackBackgroundEvents: Bool public var longTaskThreshold: TimeInterval? + public var appHangThreshold: TimeInterval? public var vitalsUpdateFrequency: VitalsFrequency? public var viewEventMapper: RUM.ViewEventMapper? public var resourceEventMapper: RUM.ResourceEventMapper? @@ -1209,7 +1480,7 @@ public struct RUM case traceWithHeaders(hostsWithHeaders: [String: Set], sampleRate: Float = 20) public init(firstPartyHostsTracing: RUM.Configuration.URLSessionTracking.FirstPartyHostsTracing? = nil,resourceAttributesProvider: RUM.ResourceAttributesProvider? = nil) [?] extension RUM.Configuration - public init(applicationID: String,sessionSampleRate: Float = 100,uiKitViewsPredicate: UIKitRUMViewsPredicate? = nil,uiKitActionsPredicate: UIKitRUMActionsPredicate? = nil,urlSessionTracking: URLSessionTracking? = nil,trackFrustrations: Bool = true,trackBackgroundEvents: Bool = false,longTaskThreshold: TimeInterval? = 0.1,vitalsUpdateFrequency: VitalsFrequency? = .average,viewEventMapper: RUM.ViewEventMapper? = nil,resourceEventMapper: RUM.ResourceEventMapper? = nil,actionEventMapper: RUM.ActionEventMapper? = nil,errorEventMapper: RUM.ErrorEventMapper? = nil,longTaskEventMapper: RUM.LongTaskEventMapper? = nil,onSessionStart: RUM.SessionListener? = nil,customEndpoint: URL? = nil,telemetrySampleRate: Float = 20) + public init(applicationID: String,sessionSampleRate: Float = 100,uiKitViewsPredicate: UIKitRUMViewsPredicate? = nil,uiKitActionsPredicate: UIKitRUMActionsPredicate? = nil,urlSessionTracking: URLSessionTracking? = nil,trackFrustrations: Bool = true,trackBackgroundEvents: Bool = false,longTaskThreshold: TimeInterval? = 0.1,appHangThreshold: TimeInterval? = nil,vitalsUpdateFrequency: VitalsFrequency? = .average,viewEventMapper: RUM.ViewEventMapper? = nil,resourceEventMapper: RUM.ResourceEventMapper? = nil,actionEventMapper: RUM.ActionEventMapper? = nil,errorEventMapper: RUM.ErrorEventMapper? = nil,longTaskEventMapper: RUM.LongTaskEventMapper? = nil,onSessionStart: RUM.SessionListener? = nil,customEndpoint: URL? = nil,telemetrySampleRate: Float = 20) [?] extension InternalExtension where ExtendedType == RUM.Configuration public var configurationTelemetrySampleRate: Float public class RUMMonitor @@ -1254,6 +1525,7 @@ public enum RUMErrorSource public protocol RUMMonitorProtocol: AnyObject func addAttribute(forKey key: AttributeKey, value: AttributeValue) func removeAttribute(forKey key: AttributeKey) + func currentSessionID(completion: @escaping (String?) -> Void) func stopSession() func startView(viewController: UIViewController,name: String?,attributes: [AttributeKey: AttributeValue]) func stopView(viewController: UIViewController,attributes: [AttributeKey: AttributeValue]) @@ -1295,9 +1567,405 @@ public static func enable() # ---------------------------------- public enum WebViewTracking - public static func enable(webView: WKWebView,hosts: Set = [],in core: DatadogCoreProtocol = CoreRegistry.default) + public struct SessionReplayConfiguration + public enum PrivacyLevel: String + case allow + case mask + case maskUserInput = "mask_user_input" + public var privacyLevel: PrivacyLevel + public init(privacyLevel: PrivacyLevel = .mask) + public static func enable(webView: WKWebView,hosts: Set = [],logsSampleRate: Float = 100,sessionReplayConfiguration: SessionReplayConfiguration? = nil,in core: DatadogCoreProtocol = CoreRegistry.default) public static func disable(webView: WKWebView) [?] extension InternalExtension where ExtendedType == WebViewTracking public class AbstractMessageEmitter - public func send(body: Any) throws - public static func messageEmitter(in core: DatadogCoreProtocol) -> AbstractMessageEmitter + public func send(body: Any, slotId: String? = nil) + public static func messageEmitter(in core: DatadogCoreProtocol,logsSampleRate: Float = 100) -> AbstractMessageEmitter + + +# ---------------------------------- +# API surface for DatadogSessionReplay: +# ---------------------------------- + +[?] extension SRTextPosition.Alignment + public init(systemTextAlignment: NSTextAlignment,vertical: SRTextPosition.Alignment.Vertical = .center) +public struct SRSegment: SRDataModel + public let application: Application + public let end: Int64 + public let hasFullSnapshot: Bool? + public let indexInView: Int64? + public let records: [SRRecord] + public let recordsCount: Int64 + public let session: Session + public let source: Source + public let start: Int64 + public let view: View + public struct Application: Codable + public let id: String + public struct Session: Codable + public let id: String + public enum Source: String, Codable + case android = "android" + case ios = "ios" + case flutter = "flutter" + case reactNative = "react-native" + public struct View: Codable + public let id: String +public struct SRShapeBorder: Codable, Hashable + public let color: String + public let width: Int64 +public struct SRContentClip: Codable, Hashable + public let bottom: Int64? + public let left: Int64? + public let right: Int64? + public let top: Int64? +public struct SRShapeStyle: Codable, Hashable + public let backgroundColor: String? + public let cornerRadius: Double? + public let opacity: Double? +public struct SRShapeWireframe: Codable, Hashable + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64 + public let id: Int64 + public let shapeStyle: SRShapeStyle? + public let type: String = "shape" + public let width: Int64 + public let x: Int64 + public let y: Int64 +public struct SRTextPosition: Codable, Hashable + public let alignment: Alignment? + public let padding: Padding? + public struct Alignment: Codable, Hashable + public let horizontal: Horizontal? + public let vertical: Vertical? + public enum Horizontal: String, Codable + case left = "left" + case right = "right" + case center = "center" + public enum Vertical: String, Codable + case top = "top" + case bottom = "bottom" + case center = "center" + public struct Padding: Codable, Hashable + public let bottom: Int64? + public let left: Int64? + public let right: Int64? + public let top: Int64? +public struct SRTextStyle: Codable, Hashable + public let color: String + public let family: String + public let size: Int64 +public struct SRTextWireframe: Codable, Hashable + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64 + public let id: Int64 + public let shapeStyle: SRShapeStyle? + public var text: String + public let textPosition: SRTextPosition? + public let textStyle: SRTextStyle + public let type: String = "text" + public let width: Int64 + public let x: Int64 + public let y: Int64 +public struct SRImageWireframe: Codable, Hashable + public var base64: String? + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64 + public let id: Int64 + public var isEmpty: Bool? + public var mimeType: String? + public var resourceId: String? + public let shapeStyle: SRShapeStyle? + public let type: String = "image" + public let width: Int64 + public let x: Int64 + public let y: Int64 +public struct SRPlaceholderWireframe: Codable, Hashable + public let clip: SRContentClip? + public let height: Int64 + public let id: Int64 + public var label: String? + public let type: String = "placeholder" + public let width: Int64 + public let x: Int64 + public let y: Int64 +public struct SRWebviewWireframe: Codable, Hashable + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64 + public let id: Int64 + public let isVisible: Bool? + public let shapeStyle: SRShapeStyle? + public let slotId: String + public let type: String = "webview" + public let width: Int64 + public let x: Int64 + public let y: Int64 +public enum SRWireframe: Codable + case shapeWireframe(value: SRShapeWireframe) + case textWireframe(value: SRTextWireframe) + case imageWireframe(value: SRImageWireframe) + case placeholderWireframe(value: SRPlaceholderWireframe) + case webviewWireframe(value: SRWebviewWireframe) + public func encode(to encoder: Encoder) throws + public init(from decoder: Decoder) throws +public struct SRFullSnapshotRecord: Codable + public let data: Data + public let timestamp: Int64 + public let type: Int64 = 10 + public struct Data: Codable + public let wireframes: [SRWireframe] +public struct SRIncrementalSnapshotRecord: Codable + public let data: Data + public let timestamp: Int64 + public let type: Int64 = 11 + public enum Data: Codable + case mutationData(value: MutationData) + case touchData(value: TouchData) + case viewportResizeData(value: ViewportResizeData) + case pointerInteractionData(value: PointerInteractionData) + public func encode(to encoder: Encoder) throws + public init(from decoder: Decoder) throws + public struct MutationData: Codable + public let adds: [Adds] + public let removes: [Removes] + public let source: Int64 = 0 + public let updates: [Updates] + public struct Adds: Codable + public let previousId: Int64? + public let wireframe: SRWireframe + public struct Removes: Codable + public let id: Int64 + public enum Updates: Codable + case textWireframeUpdate(value: TextWireframeUpdate) + case shapeWireframeUpdate(value: ShapeWireframeUpdate) + case imageWireframeUpdate(value: ImageWireframeUpdate) + case placeholderWireframeUpdate(value: PlaceholderWireframeUpdate) + case webviewWireframeUpdate(value: WebviewWireframeUpdate) + public func encode(to encoder: Encoder) throws + public init(from decoder: Decoder) throws + public struct TextWireframeUpdate: Codable + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64? + public let id: Int64 + public let shapeStyle: SRShapeStyle? + public var text: String? + public let textPosition: SRTextPosition? + public let textStyle: SRTextStyle? + public let type: String = "text" + public let width: Int64? + public let x: Int64? + public let y: Int64? + public struct ShapeWireframeUpdate: Codable + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64? + public let id: Int64 + public let shapeStyle: SRShapeStyle? + public let type: String = "shape" + public let width: Int64? + public let x: Int64? + public let y: Int64? + public struct ImageWireframeUpdate: Codable + public var base64: String? + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64? + public let id: Int64 + public var isEmpty: Bool? + public var mimeType: String? + public var resourceId: String? + public let shapeStyle: SRShapeStyle? + public let type: String = "image" + public let width: Int64? + public let x: Int64? + public let y: Int64? + public struct PlaceholderWireframeUpdate: Codable + public let clip: SRContentClip? + public let height: Int64? + public let id: Int64 + public var label: String? + public let type: String = "placeholder" + public let width: Int64? + public let x: Int64? + public let y: Int64? + public struct WebviewWireframeUpdate: Codable + public let border: SRShapeBorder? + public let clip: SRContentClip? + public let height: Int64? + public let id: Int64 + public let isVisible: Bool? + public let shapeStyle: SRShapeStyle? + public let slotId: String + public let type: String = "webview" + public let width: Int64? + public let x: Int64? + public let y: Int64? + public struct TouchData: Codable + public let positions: [Positions]? + public let source: Int64 = 2 + public struct Positions: Codable + public let id: Int64 + public let timestamp: Int64 + public let x: Int64 + public let y: Int64 + public struct ViewportResizeData: Codable + public let height: Int64 + public let source: Int64 = 4 + public let width: Int64 + public struct PointerInteractionData: Codable + public let pointerEventType: PointerEventType + public let pointerId: Int64 + public let pointerType: PointerType + public let source: Int64 = 9 + public let x: Double + public let y: Double + public enum PointerEventType: String, Codable + case down = "down" + case up = "up" + case move = "move" + public enum PointerType: String, Codable + case mouse = "mouse" + case touch = "touch" + case pen = "pen" +public struct SRMetaRecord: Codable + public let data: Data + public let slotId: String? + public let timestamp: Int64 + public let type: Int64 = 4 + public struct Data: Codable + public let height: Int64 + public let href: String? + public let width: Int64 +public struct SRFocusRecord: Codable + public let data: Data + public let slotId: String? + public let timestamp: Int64 + public let type: Int64 = 6 + public struct Data: Codable + public let hasFocus: Bool +public struct SRViewEndRecord: Codable + public let slotId: String? + public let timestamp: Int64 + public let type: Int64 = 7 +public struct SRVisualViewportRecord: Codable + public let data: Data + public let slotId: String? + public let timestamp: Int64 + public let type: Int64 = 8 + public struct Data: Codable + public let height: Double + public let offsetLeft: Double + public let offsetTop: Double + public let pageLeft: Double + public let pageTop: Double + public let scale: Double + public let width: Double +public enum SRRecord: Codable + case fullSnapshotRecord(value: SRFullSnapshotRecord) + case incrementalSnapshotRecord(value: SRIncrementalSnapshotRecord) + case metaRecord(value: SRMetaRecord) + case focusRecord(value: SRFocusRecord) + case viewEndRecord(value: SRViewEndRecord) + case visualViewportRecord(value: SRVisualViewportRecord) + public func encode(to encoder: Encoder) throws + public init(from decoder: Decoder) throws +public typealias WireframeID = NodeID +public class SessionReplayWireframesBuilder + public struct FontOverride + public init(size: CGFloat?) + public func createShapeWireframe(id: WireframeID,frame: CGRect,clip: SRContentClip? = nil,borderColor: CGColor? = nil,borderWidth: CGFloat? = nil,backgroundColor: CGColor? = nil,cornerRadius: CGFloat? = nil,opacity: CGFloat? = nil) -> SRWireframe + public func createImageWireframe(resourceId: String,id: WireframeID,frame: CGRect,mimeType: String = "png",clip: SRContentClip? = nil,borderColor: CGColor? = nil,borderWidth: CGFloat? = nil,backgroundColor: CGColor? = nil,cornerRadius: CGFloat? = nil,opacity: CGFloat? = nil) -> SRWireframe + public func createTextWireframe(id: WireframeID,frame: CGRect,text: String,textFrame: CGRect? = nil,textAlignment: SRTextPosition.Alignment? = nil,clip: SRContentClip? = nil,textColor: CGColor? = nil,font: UIFont? = nil,fontOverride: FontOverride? = nil,fontScalingEnabled: Bool = false,borderColor: CGColor? = nil,borderWidth: CGFloat? = nil,backgroundColor: CGColor? = nil,cornerRadius: CGFloat? = nil,opacity: CGFloat? = nil) -> SRWireframe + public func createPlaceholderWireframe(id: Int64,frame: CGRect,label: String,clip: SRContentClip? = nil) -> SRWireframe + public func visibleWebViewWireframe(id: Int,frame: CGRect,clip: SRContentClip? = nil,borderColor: CGColor? = nil,borderWidth: CGFloat? = nil,backgroundColor: CGColor? = nil,cornerRadius: CGFloat? = nil,opacity: CGFloat? = nil) -> SRWireframe +[?] extension SRContentClip + public static func create(bottom: Int64?,left: Int64?,right: Int64?,top: Int64?) -> SRContentClip +[?] extension SRImageWireframe + public static func == (lhs: SRImageWireframe, rhs: SRImageWireframe) -> Bool + public func hash(into hasher: inout Hasher) +public protocol SessionReplayTextObfuscating + func mask(text: String) -> String +public typealias SessionReplayPrivacyLevel = SessionReplay.Configuration.PrivacyLevel +public extension SessionReplay.Configuration.PrivacyLevel + var sensitiveTextObfuscator: SessionReplayTextObfuscating + var inputAndOptionTextObfuscator: SessionReplayTextObfuscating + var staticTextObfuscator: SessionReplayTextObfuscating + var hintTextObfuscator: SessionReplayTextObfuscating +public class Recorder: Recording + public struct Context: Equatable + public let privacy: SessionReplayPrivacyLevel + deinit +public typealias NodeID = Int64 +public final class NodeIDGenerator + public func nodeID(view: UIView, nodeRecorder: SessionReplayNodeRecorder) -> NodeID +public protocol SessionReplayNodeRecorder + func semantics(of view: UIView, with attributes: SessionReplayViewAttributes, in context: SessionReplayViewTreeRecordingContext) -> SessionReplayNodeSemantics? + var identifier: UUID +public protocol SessionReplayNodeWireframesBuilder + var wireframeRect: CGRect + func buildWireframes(with builder: SessionReplayWireframesBuilder) -> [SRWireframe] +public struct SessionReplayViewTreeRecordingContext + public let recorder: Recorder.Context + public let ids: NodeIDGenerator +public struct SessionReplayNode + public let viewAttributes: SessionReplayViewAttributes + public let wireframesBuilder: SessionReplayNodeWireframesBuilder + public init(viewAttributes: SessionReplayViewAttributes, wireframesBuilder: SessionReplayNodeWireframesBuilder) +public protocol SessionReplayResource + func calculateIdentifier() -> String + func calculateData() -> Data +public struct SessionReplayViewAttributes: Equatable + public let frame: CGRect + public let backgroundColor: CGColor? + public let layerBorderColor: CGColor? + public let layerBorderWidth: CGFloat + public let layerCornerRadius: CGFloat + public let alpha: CGFloat +public protocol SessionReplayNodeSemantics + static var importance: Int + var subtreeStrategy: SessionReplayNodeSubtreeStrategy + var nodes: [SessionReplayNode] + var resources: [SessionReplayResource] +public enum SessionReplayNodeSubtreeStrategy + case record + case ignore +public struct SessionReplayInvisibleElement: SessionReplayNodeSemantics + public static let importance: Int = 0 + public let subtreeStrategy: SessionReplayNodeSubtreeStrategy + public let nodes: [SessionReplayNode] = [] + public let resources: [SessionReplayResource] = [] + public static let constant = SessionReplayInvisibleElement() +public struct SessionReplaySpecificElement: SessionReplayNodeSemantics + public static let importance: Int = .max + public let subtreeStrategy: SessionReplayNodeSubtreeStrategy + public let nodes: [SessionReplayNode] + public let resources: [SessionReplayResource] + public init(subtreeStrategy: SessionReplayNodeSubtreeStrategy,nodes: [SessionReplayNode],resources: [SessionReplayResource] = []) +public final class DDSessionReplay: NSObject + public static func enable(with configuration: DDSessionReplayConfiguration) +public final class DDSessionReplayConfiguration: NSObject + @objc public var replaySampleRate: Float + @objc public var defaultPrivacyLevel: DDSessionReplayConfigurationPrivacyLevel + @objc public var customEndpoint: URL? + public required init(replaySampleRate: Float) +public enum DDSessionReplayConfigurationPrivacyLevel: Int + case allow + case mask + case maskUserInput +public enum SessionReplay + public static func enable(with configuration: SessionReplay.Configuration, in core: DatadogCoreProtocol = CoreRegistry.default) +[?] extension SessionReplay + public struct Configuration + public var replaySampleRate: Float + public var defaultPrivacyLevel: PrivacyLevel + public enum PrivacyLevel: String + case allow + case mask + case maskUserInput = "mask_user_input" + public var customEndpoint: URL? + public init(replaySampleRate: Float,defaultPrivacyLevel: PrivacyLevel = .mask,customEndpoint: URL? = nil) + public mutating func setAdditionalNodeRecorders(_ additionalNodeRecorders: [SessionReplayNodeRecorder]) diff --git a/bitrise.yml b/bitrise.yml index e6a5f01ac1..718c34f719 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -360,6 +360,18 @@ workflows: -project "$BITRISE_SOURCE_DIR/dependency-manager-tests/spm/SPMProject.xcodeproj" \ -destination "platform=macOS,variant=Mac Catalyst" \ | xcpretty + - script: + title: Check macOS compatibility (build SPMProject for macOS) + run_if: '{{enveq "DD_RUN_SMOKE_TESTS" "1"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -euxo pipefail + + xcodebuild build -scheme "App macOS" \ + -project "$BITRISE_SOURCE_DIR/dependency-manager-tests/spm/SPMProject.xcodeproj" \ + -destination "platform=macOS" \ + | xcpretty - xcode-test: title: Run SPMProject iOS tests run_if: '{{enveq "DD_RUN_SMOKE_TESTS" "1"}}' diff --git a/dependency-manager-tests/spm/App macOS/App_macOS.entitlements b/dependency-manager-tests/spm/App macOS/App_macOS.entitlements new file mode 100644 index 0000000000..a11054c1df --- /dev/null +++ b/dependency-manager-tests/spm/App macOS/App_macOS.entitlements @@ -0,0 +1,2 @@ +com.apple.security.app-sandbox + diff --git a/dependency-manager-tests/spm/App macOS/App_macOSApp.swift b/dependency-manager-tests/spm/App macOS/App_macOSApp.swift new file mode 100644 index 0000000000..123623e06c --- /dev/null +++ b/dependency-manager-tests/spm/App macOS/App_macOSApp.swift @@ -0,0 +1,20 @@ +/* +* 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 SwiftUI + +@main +struct App_macOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } + + init() { + DatadogSetup.initialize() + } +} diff --git a/dependency-manager-tests/spm/App macOS/ContentView.swift b/dependency-manager-tests/spm/App macOS/ContentView.swift new file mode 100644 index 0000000000..1fc7b91149 --- /dev/null +++ b/dependency-manager-tests/spm/App macOS/ContentView.swift @@ -0,0 +1,18 @@ +/* +* 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 SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + Text("Testing...") + } + .padding() + } +} diff --git a/dependency-manager-tests/spm/App macOSUITests/App_macOSUITests.swift b/dependency-manager-tests/spm/App macOSUITests/App_macOSUITests.swift new file mode 100644 index 0000000000..b0eacadf1c --- /dev/null +++ b/dependency-manager-tests/spm/App macOSUITests/App_macOSUITests.swift @@ -0,0 +1,15 @@ +/* +* 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 + +final class App_macOSUITests: XCTestCase { + func testExample() throws { + let app = XCUIApplication() + app.launch() + XCTAssert(app.staticTexts["Testing..."].exists) + } +} diff --git a/dependency-manager-tests/spm/App/ViewController.swift b/dependency-manager-tests/spm/App/ViewController.swift index a1b1e258c4..5e1a17c4d5 100644 --- a/dependency-manager-tests/spm/App/ViewController.swift +++ b/dependency-manager-tests/spm/App/ViewController.swift @@ -5,46 +5,19 @@ */ import UIKit -import DatadogCore -import DatadogLogs -import DatadogTrace import DatadogRUM -import DatadogCrashReporting -import DatadogSessionReplay // it should compile for iOS and tvOS, but APIs are only available on iOS import DatadogObjc +import DatadogSessionReplay // it should compile for iOS and tvOS, but APIs are only available on iOS internal class ViewController: UIViewController { - private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional - override func viewDidLoad() { super.viewDidLoad() - - Datadog.initialize( - with: Datadog.Configuration(clientToken: "abc", env: "tests"), - trackingConsent: .granted - ) - - Logs.enable() - - CrashReporting.enable() - - self.logger = Logger.create( - with: Logger.Configuration( - remoteSampleRate: 0, - consoleLogFormat: .short - ) - ) + DatadogSetup.initialize() // RUM APIs must be visible: RUM.enable(with: .init(applicationID: "app-id")) RUMMonitor.shared().startView(viewController: self) - - // Trace APIs must be visible: - Trace.enable() - - logger.info("It works") - _ = Tracer.shared().startSpan(operationName: "this too") - + #if os(iOS) // Session Replay API must be visible: SessionReplay.enable(with: .init(replaySampleRate: 0)) diff --git a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj index f4720f51a7..ecf6a4b36c 100644 --- a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj +++ b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj @@ -14,6 +14,17 @@ 61C363F124374D6100C4D4E6 /* AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C363F024374D6100C4D4E6 /* AppTests.swift */; }; 61C363FC24374D6100C4D4E6 /* AppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C363FB24374D6100C4D4E6 /* AppUITests.swift */; }; 9E73374026B0123500917C24 /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 9E73373F26B0123500917C24 /* DatadogCrashReporting */; }; + A71398892BD127790050A54E /* App_macOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71398882BD127790050A54E /* App_macOSApp.swift */; }; + A713988B2BD127790050A54E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A713988A2BD127790050A54E /* ContentView.swift */; }; + A71398A52BD1277A0050A54E /* App_macOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71398A42BD1277A0050A54E /* App_macOSUITests.swift */; }; + A71398B22BD12B6D0050A54E /* DatadogSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71398B12BD12B6D0050A54E /* DatadogSetup.swift */; }; + A71398B32BD12B6D0050A54E /* DatadogSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71398B12BD12B6D0050A54E /* DatadogSetup.swift */; }; + A71398B42BD12B6D0050A54E /* DatadogSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71398B12BD12B6D0050A54E /* DatadogSetup.swift */; }; + A71398B62BD12BC60050A54E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = A71398B52BD12BC60050A54E /* DatadogCore */; }; + A71398B82BD12BC60050A54E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = A71398B72BD12BC60050A54E /* DatadogCrashReporting */; }; + A71398BA2BD12BC60050A54E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = A71398B92BD12BC60050A54E /* DatadogLogs */; }; + A71398BC2BD12BC60050A54E /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = A71398BB2BD12BC60050A54E /* DatadogTrace */; }; + A71398BE2BD12BC60050A54E /* DatadogWebViewTracking in Frameworks */ = {isa = PBXBuildFile; productRef = A71398BD2BD12BC60050A54E /* DatadogWebViewTracking */; }; D22919312B76589B00C38A18 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D22919302B76589B00C38A18 /* PrivacyInfo.xcprivacy */; }; D22919322B76589B00C38A18 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D22919302B76589B00C38A18 /* PrivacyInfo.xcprivacy */; }; D2344E1F29ACDE61007F5BD2 /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = D2344E1E29ACDE61007F5BD2 /* DatadogLogs */; }; @@ -50,6 +61,13 @@ remoteGlobalIDString = 61C363D524374D5F00C4D4E6; remoteInfo = SPMProject; }; + A71398A12BD1277A0050A54E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61C363CE24374D5F00C4D4E6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A71398852BD127790050A54E; + remoteInfo = "App macOS"; + }; D23BF61827CCCE2300BB4CCD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61C363CE24374D5F00C4D4E6 /* Project object */; @@ -103,6 +121,13 @@ 61C363FD24374D6100C4D4E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61CE5FD52461D3C2005EA621 /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 61CE5FD62461D3C2005EA621 /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; + A71398862BD127790050A54E /* App macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "App macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + A71398882BD127790050A54E /* App_macOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App_macOSApp.swift; sourceTree = ""; }; + A713988A2BD127790050A54E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A71398A02BD1277A0050A54E /* App macOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "App macOSUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + A71398A42BD1277A0050A54E /* App_macOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App_macOSUITests.swift; sourceTree = ""; }; + A71398B12BD12B6D0050A54E /* DatadogSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogSetup.swift; sourceTree = ""; }; + A7DFE01A2BD131D90024E5E1 /* App_macOS.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = App_macOS.entitlements; sourceTree = ""; }; D22919302B76589B00C38A18 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; D23BF5FE27CCCC3300BB4CCD /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; D23BF60A27CCCCF800BB4CCD /* App tvOS UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "App tvOS UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -139,6 +164,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A71398832BD127790050A54E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A71398B82BD12BC60050A54E /* DatadogCrashReporting in Frameworks */, + A71398BE2BD12BC60050A54E /* DatadogWebViewTracking in Frameworks */, + A71398BA2BD12BC60050A54E /* DatadogLogs in Frameworks */, + A71398B62BD12BC60050A54E /* DatadogCore in Frameworks */, + A71398BC2BD12BC60050A54E /* DatadogTrace in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A713989D2BD1277A0050A54E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D23BF5F627CCCC3300BB4CCD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -173,9 +217,12 @@ isa = PBXGroup; children = ( 61CE5FD42461D3C2005EA621 /* xcconfigs */, + A787CA7D2BD277190053CEBE /* Shared */, 61C363D824374D5F00C4D4E6 /* App */, 61C363EF24374D6100C4D4E6 /* AppTests */, 61C363FA24374D6100C4D4E6 /* AppUITests */, + A71398872BD127790050A54E /* App macOS */, + A71398A32BD1277A0050A54E /* App macOSUITests */, 61C363D724374D5F00C4D4E6 /* Products */, 9EC32C6024E596C20063BCCE /* Frameworks */, ); @@ -190,6 +237,8 @@ D23BF5FE27CCCC3300BB4CCD /* App.app */, D23BF60A27CCCCF800BB4CCD /* App tvOS UITests.xctest */, D23BF61627CCCD5700BB4CCD /* App tvOS Tests.xctest */, + A71398862BD127790050A54E /* App macOS.app */, + A71398A02BD1277A0050A54E /* App macOSUITests.xctest */, ); name = Products; sourceTree = ""; @@ -241,6 +290,32 @@ name = Frameworks; sourceTree = ""; }; + A71398872BD127790050A54E /* App macOS */ = { + isa = PBXGroup; + children = ( + A7DFE01A2BD131D90024E5E1 /* App_macOS.entitlements */, + A71398882BD127790050A54E /* App_macOSApp.swift */, + A713988A2BD127790050A54E /* ContentView.swift */, + ); + path = "App macOS"; + sourceTree = ""; + }; + A71398A32BD1277A0050A54E /* App macOSUITests */ = { + isa = PBXGroup; + children = ( + A71398A42BD1277A0050A54E /* App_macOSUITests.swift */, + ); + path = "App macOSUITests"; + sourceTree = ""; + }; + A787CA7D2BD277190053CEBE /* Shared */ = { + isa = PBXGroup; + children = ( + A71398B12BD12B6D0050A54E /* DatadogSetup.swift */, + ); + path = Shared; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -309,6 +384,48 @@ productReference = 61C363F724374D6100C4D4E6 /* App iOS UITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + A71398852BD127790050A54E /* App macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A71398AE2BD1277A0050A54E /* Build configuration list for PBXNativeTarget "App macOS" */; + buildPhases = ( + A71398822BD127790050A54E /* Sources */, + A71398832BD127790050A54E /* Frameworks */, + A71398842BD127790050A54E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "App macOS"; + packageProductDependencies = ( + A71398B52BD12BC60050A54E /* DatadogCore */, + A71398B72BD12BC60050A54E /* DatadogCrashReporting */, + A71398B92BD12BC60050A54E /* DatadogLogs */, + A71398BB2BD12BC60050A54E /* DatadogTrace */, + A71398BD2BD12BC60050A54E /* DatadogWebViewTracking */, + ); + productName = "App macOS"; + productReference = A71398862BD127790050A54E /* App macOS.app */; + productType = "com.apple.product-type.application"; + }; + A713989F2BD1277A0050A54E /* App macOSUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A71398B02BD1277A0050A54E /* Build configuration list for PBXNativeTarget "App macOSUITests" */; + buildPhases = ( + A713989C2BD1277A0050A54E /* Sources */, + A713989D2BD1277A0050A54E /* Frameworks */, + A713989E2BD1277A0050A54E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A71398A22BD1277A0050A54E /* PBXTargetDependency */, + ); + name = "App macOSUITests"; + productName = "App macOSUITests"; + productReference = A71398A02BD1277A0050A54E /* App macOSUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; D23BF5EF27CCCC3300BB4CCD /* App tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = D23BF5FB27CCCC3300BB4CCD /* Build configuration list for PBXNativeTarget "App tvOS" */; @@ -378,7 +495,7 @@ 61C363CE24374D5F00C4D4E6 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1140; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1310; ORGANIZATIONNAME = Datadog; TargetAttributes = { @@ -393,6 +510,13 @@ CreatedOnToolsVersion = 11.4; TestTargetID = 61C363D524374D5F00C4D4E6; }; + A71398852BD127790050A54E = { + CreatedOnToolsVersion = 15.3; + }; + A713989F2BD1277A0050A54E = { + CreatedOnToolsVersion = 15.3; + TestTargetID = A71398852BD127790050A54E; + }; D23BF60027CCCCF800BB4CCD = { TestTargetID = D23BF5EF27CCCC3300BB4CCD; }; @@ -423,6 +547,8 @@ D23BF5EF27CCCC3300BB4CCD /* App tvOS */, D23BF60C27CCCD5700BB4CCD /* App tvOS Tests */, D23BF60027CCCCF800BB4CCD /* App tvOS UITests */, + A71398852BD127790050A54E /* App macOS */, + A713989F2BD1277A0050A54E /* App macOSUITests */, ); }; /* End PBXProject section */ @@ -450,6 +576,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A71398842BD127790050A54E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A713989E2BD1277A0050A54E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D23BF5F827CCCC3300BB4CCD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -522,6 +662,7 @@ files = ( 61C363DE24374D5F00C4D4E6 /* ViewController.swift in Sources */, 61C363DA24374D5F00C4D4E6 /* AppDelegate.swift in Sources */, + A71398B22BD12B6D0050A54E /* DatadogSetup.swift in Sources */, 61C363DC24374D5F00C4D4E6 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -542,12 +683,31 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A71398822BD127790050A54E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A713988B2BD127790050A54E /* ContentView.swift in Sources */, + A71398892BD127790050A54E /* App_macOSApp.swift in Sources */, + A71398B42BD12B6D0050A54E /* DatadogSetup.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A713989C2BD1277A0050A54E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A71398A52BD1277A0050A54E /* App_macOSUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D23BF5F227CCCC3300BB4CCD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D23BF5F327CCCC3300BB4CCD /* ViewController.swift in Sources */, D23BF5F427CCCC3300BB4CCD /* AppDelegate.swift in Sources */, + A71398B32BD12B6D0050A54E /* DatadogSetup.swift in Sources */, D23BF5F527CCCC3300BB4CCD /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -581,6 +741,11 @@ target = 61C363D524374D5F00C4D4E6 /* App iOS */; targetProxy = 61C363F824374D6100C4D4E6 /* PBXContainerItemProxy */; }; + A71398A22BD1277A0050A54E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A71398852BD127790050A54E /* App macOS */; + targetProxy = A71398A12BD1277A0050A54E /* PBXContainerItemProxy */; + }; D23BF61927CCCE2300BB4CCD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D23BF5EF27CCCC3300BB4CCD /* App tvOS */; @@ -854,6 +1019,130 @@ }; name = Release; }; + A71398A82BD1277A0050A54E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "App macOS/App_macOS.entitlements"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Datadog. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SPMProjectMacOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A71398A92BD1277A0050A54E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "App macOS/App_macOS.entitlements"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Datadog. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SPMProjectMacOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + A71398AC2BD1277A0050A54E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SPMProjectMacOSUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "App macOS"; + }; + name = Debug; + }; + A71398AD2BD1277A0050A54E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SPMProjectMacOSUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "App macOS"; + }; + name = Release; + }; D23BF5FC27CCCC3300BB4CCD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1031,6 +1320,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A71398AE2BD1277A0050A54E /* Build configuration list for PBXNativeTarget "App macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A71398A82BD1277A0050A54E /* Debug */, + A71398A92BD1277A0050A54E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A71398B02BD1277A0050A54E /* Build configuration list for PBXNativeTarget "App macOSUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A71398AC2BD1277A0050A54E /* Debug */, + A71398AD2BD1277A0050A54E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D23BF5FB27CCCC3300BB4CCD /* Build configuration list for PBXNativeTarget "App tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1065,7 +1372,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DataDog/dd-sdk-ios"; requirement = { - branch = GIT_REFERENCE; + branch = "GIT_REFERENCE"; kind = branch; }; }; @@ -1073,7 +1380,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DataDog/dd-sdk-ios"; requirement = { - branch = GIT_REFERENCE; + branch = "GIT_REFERENCE"; kind = branch; }; }; @@ -1090,6 +1397,31 @@ package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; productName = DatadogCrashReporting; }; + A71398B52BD12BC60050A54E /* DatadogCore */ = { + isa = XCSwiftPackageProductDependency; + package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCore; + }; + A71398B72BD12BC60050A54E /* DatadogCrashReporting */ = { + isa = XCSwiftPackageProductDependency; + package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCrashReporting; + }; + A71398B92BD12BC60050A54E /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogLogs; + }; + A71398BB2BD12BC60050A54E /* DatadogTrace */ = { + isa = XCSwiftPackageProductDependency; + package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogTrace; + }; + A71398BD2BD12BC60050A54E /* DatadogWebViewTracking */ = { + isa = XCSwiftPackageProductDependency; + package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogWebViewTracking; + }; D2344E1E29ACDE61007F5BD2 /* DatadogLogs */ = { isa = XCSwiftPackageProductDependency; package = 9E73373E26B0123500917C24 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; diff --git a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/App macOS.xcscheme b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/App macOS.xcscheme new file mode 100644 index 0000000000..9e772b47d7 --- /dev/null +++ b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/App macOS.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dependency-manager-tests/spm/Shared/DatadogSetup.swift b/dependency-manager-tests/spm/Shared/DatadogSetup.swift new file mode 100644 index 0000000000..43172f15e2 --- /dev/null +++ b/dependency-manager-tests/spm/Shared/DatadogSetup.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-Present Datadog, Inc. +*/ + +import DatadogCore +import DatadogLogs +import DatadogTrace +import DatadogCrashReporting + +enum DatadogSetup { + static var logger: LoggerProtocol? + static func initialize() { + Datadog.initialize( + with: Datadog.Configuration(clientToken: "abc", env: "tests"), + trackingConsent: .granted + ) + + Logs.enable() + + CrashReporting.enable() + + logger = Logger.create( + with: Logger.Configuration( + remoteSampleRate: 0, + consoleLogFormat: .short + ) + ) + + // Trace APIs must be visible: + Trace.enable() + + logger?.info("It works") + let span = Tracer.shared().startSpan(operationName: "this too") + span.finish() + } +}