diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aaf181dfc..052ae4f300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Unreleased +# 2.12.0 / 03-06-2024 + +- [IMPROVEMENT] Crash errors now include up-to-date global RUM attributes. See [#1834][] +- [FEATURE] `DatadogTrace` now supports OpenTelemetry. See [#1828][] +- [FIX] Fix crash on accessing request.allHTTPHeaderFields. See [#1843][] +- [FEATURE] Support for trace context injection configuration to allow selective injection. See [#1835][] +- [FEATURE] `DatadogWebViewTracking` is now available for Obj-C. See [#1854][] +- [FEATURE] RUM "stop session", "get session ID" and "evaluate feature flag" APIs are now available for Obj-C. See [#1853][] + # 2.11.0 / 08-05-2024 - [FEATURE] `DatadogTrace` now supports head-based sampling. See [#1794][] @@ -655,11 +664,17 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#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 +[#1843]: https://github.com/DataDog/dd-sdk-ios/pull/1843 [#1798]: https://github.com/DataDog/dd-sdk-ios/pull/1798 [#1776]: https://github.com/DataDog/dd-sdk-ios/pull/1776 +[#1834]: https://github.com/DataDog/dd-sdk-ios/pull/1834 [#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 [#1803]: https://github.com/DataDog/dd-sdk-ios/pull/1803 +[#1853]: https://github.com/DataDog/dd-sdk-ios/pull/1853 [#1807]: https://github.com/DataDog/dd-sdk-ios/pull/1807 +[#1854]: https://github.com/DataDog/dd-sdk-ios/pull/1854 +[#1828]: https://github.com/DataDog/dd-sdk-ios/pull/1828 +[#1835]: https://github.com/DataDog/dd-sdk-ios/pull/1835 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu @@ -689,4 +704,4 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [@dfed]: https://github.com/dfed [@cltnschlosser]: https://github.com/cltnschlosser [@alexfanatics]: https://github.com/alexfanatics -[@changm4n]: https://github.com/changm4n +[@changm4n]: https://github.com/changm4n \ No newline at end of file diff --git a/Cartfile b/Cartfile index 0299765af6..31328b20d2 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1,2 @@ -github "microsoft/plcrashreporter" ~> 1.11.1 +github "microsoft/plcrashreporter" ~> 1.11.2 +binary "https://raw.githubusercontent.com/DataDog/opentelemetry-swift-packages/main/OpenTelemetryApi.json" ~> 1.6.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 8e3025f36c..613647a075 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1,2 @@ -github "microsoft/plcrashreporter" "1.11.1" +binary "https://raw.githubusercontent.com/DataDog/opentelemetry-swift-packages/main/OpenTelemetryApi.json" "1.6.0" +github "microsoft/plcrashreporter" "1.11.2" diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 407bcaca8e..b1768c336c 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 1434A4612B7F73110072E3BB /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; + 1434A4622B7F73110072E3BB /* OpenTelemetryApi.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1434A4632B7F73170072E3BB /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; + 1434A4642B7F73170072E3BB /* OpenTelemetryApi.xcframework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1434A4662B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; + 1434A4672B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; 3C0D5DD72A543B3B00446CF9 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DD62A543B3B00446CF9 /* Event.swift */; }; 3C0D5DD82A543B3B00446CF9 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DD62A543B3B00446CF9 /* Event.swift */; }; 3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */; }; @@ -27,8 +33,32 @@ 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 */; }; + 3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */; }; + 3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */; }; + 3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; }; + 3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; }; + 3C33E4072BEE35A8003B2988 /* RUMContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */; }; 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; + 3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; + 3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; + 3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; }; + 3C5D636D2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; }; + 3C5D691F2B76825500C4E07E /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; + 3C5D69222B76826000C4E07E /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; + 3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; }; + 3C6C7FE82B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; }; + 3C6C7FE92B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */; }; + 3C6C7FEA2B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */; }; + 3C6C7FEB2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */; }; + 3C6C7FEC2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */; }; + 3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */; }; + 3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */; }; + 3C6C7FFB2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */; }; + 3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */; }; + 3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */; }; + 3C6C7FFE2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */; }; + 3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */; }; + 3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */; }; 3C74305C29FBC0480053B80F /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; 3C85D42129F7C5C900AFF894 /* WebViewTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */; }; 3C85D42A29F7C70300AFF894 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; @@ -37,10 +67,22 @@ 3C9B27252B9F174700569C07 /* SpanID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9B27242B9F174700569C07 /* SpanID.swift */; }; 3C9B27262B9F174700569C07 /* SpanID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9B27242B9F174700569C07 /* SpanID.swift */; }; 3C9C6BB429F7C0C000581C43 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + 3CA8525F2BF2073800B52CBA /* TraceContextInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */; }; + 3CA852602BF2073800B52CBA /* TraceContextInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */; }; + 3CA852642BF2148200B52CBA /* TraceContextInjection+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */; }; + 3CA852652BF2148400B52CBA /* TraceContextInjection+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */; }; + 3CB012DD2B482E0400557951 /* NOPOTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */; }; + 3CB012DE2B482E0400557951 /* NOPOTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */; }; + 3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */; }; + 3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */; }; 3CBDE6742AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */; }; 3CBDE6752AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */; }; 3CBDE68A2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; 3CBDE68B2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; + 3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */; }; + 3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */; }; + 3CC6AD1D2B4F07FA00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */; }; + 3CC6AD1E2B4F07FB00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */; }; 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; 3CCCA5C72ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */; }; @@ -49,8 +91,14 @@ 3CCECDB02BC688120013C125 /* SpanIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */; }; 3CCECDB22BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; 3CCECDB32BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; + 3CDA3F7E2BCD866D005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */; }; + 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */; }; 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3CE11A1229F7BE0900202522 /* DatadogWebViewTracking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3CF673362B4807490016CE17 /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF673352B4807490016CE17 /* OTelSpanTests.swift */; }; + 3CF673372B4807490016CE17 /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF673352B4807490016CE17 /* OTelSpanTests.swift */; }; + 3CFF5D492B555F4F00FC483A /* OTelTracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */; }; + 3CFF5D4A2B555F4F00FC483A /* OTelTracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */; }; 49274906288048B500ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; 49274907288048B800ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; 49D8C0B72AC5D2160075E427 /* RUM+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */; }; @@ -179,8 +227,8 @@ 61054FD32A6EE1BA00AAA894 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F902A6EE1BA00AAA894 /* MultipartFormDataTests.swift */; }; 61054FD42A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F912A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift */; }; 61054FD52A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F932A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift */; }; - 610ABD4C2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610ABD4B2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift */; }; - 610ABD4D2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610ABD4B2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift */; }; + 610ABD4C2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */; }; + 610ABD4D2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */; }; 61112F8E2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */; }; 61112F8F2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */; }; 6111C58225C0081F00F5C4A2 /* RUMDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6111C58125C0081F00F5C4A2 /* RUMDataModels+objc.swift */; }; @@ -222,6 +270,8 @@ 61133C702423993200786299 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; 6115299725E3BEF9004F740E /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */; }; 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */; }; + 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; + 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; 6121627C247D220500AC5D67 /* TracingWithLoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216279247D21FE00AC5D67 /* TracingWithLoggingIntegrationTests.swift */; }; 61216B762666DDA10089DCD1 /* LoggerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B752666DDA10089DCD1 /* LoggerConfigurationTests.swift */; }; 61216B7B2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B7A2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift */; }; @@ -261,8 +311,8 @@ 6133D2012A6EDB7700384BEF /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; 6133D20B2A6EDBC100384BEF /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61345612244756E300E7DA6B /* PerformancePresetTests.swift */; }; - 6134CDB12A691E850061CCD9 /* CoreMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134CDB02A691E850061CCD9 /* CoreMetricsTests.swift */; }; - 6134CDB22A691E850061CCD9 /* CoreMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134CDB02A691E850061CCD9 /* CoreMetricsTests.swift */; }; + 6134CDB12A691E850061CCD9 /* BatchMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */; }; + 6134CDB22A691E850061CCD9 /* BatchMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */; }; 61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */; }; 6136CB4A2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */; }; 6136CB4B2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */; }; @@ -274,8 +324,8 @@ 613F9C192BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C172BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift */; }; 613F9C1B2BB03188007C7606 /* FeatureScopeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C1A2BB03188007C7606 /* FeatureScopeMock.swift */; }; 613F9C1C2BB03188007C7606 /* FeatureScopeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C1A2BB03188007C7606 /* FeatureScopeMock.swift */; }; - 614396722A67D74F00197326 /* CoreMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* CoreMetrics.swift */; }; - 614396732A67D74F00197326 /* CoreMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* CoreMetrics.swift */; }; + 614396722A67D74F00197326 /* BatchMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* BatchMetrics.swift */; }; + 614396732A67D74F00197326 /* BatchMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* BatchMetrics.swift */; }; 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */; }; 61441C0C24616DE9003D8BB8 /* Main iOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0A24616DE9003D8BB8 /* Main iOS.storyboard */; }; 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; @@ -294,6 +344,8 @@ 614798A22A45A48F0095CB02 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; }; 614798A32A45A4980095CB02 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; }; 6147E3B3270486920092BC9F /* TraceConfigurationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6147E3B2270486920092BC9F /* TraceConfigurationE2ETests.swift */; }; + 614A708E2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */; }; + 614A708F2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */; }; 614B78ED296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */; }; 614B78EE296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */; }; 614B78F1296D7B63009C6B92 /* LowPowerModePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EC296D7B63009C6B92 /* LowPowerModePublisherTests.swift */; }; @@ -305,6 +357,8 @@ 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 */; }; + 6156A9072BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */; }; + 6156A9082BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.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 */; }; @@ -366,6 +420,12 @@ 6172472725D673D7007085B3 /* CrashContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6172472625D673D7007085B3 /* CrashContextTests.swift */; }; 617247AF25DA9BEA007085B3 /* CrashReportingObjcHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 617247AE25DA9BEA007085B3 /* CrashReportingObjcHelpers.m */; }; 617247B825DAB0E2007085B3 /* DDCrashReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */; }; + 6174D6042BFB9AB600EC7469 /* WebViewTracking+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6032BFB9AB600EC7469 /* WebViewTracking+objc.swift */; }; + 6174D6062BFB9D6400EC7469 /* DDWebViewTracking+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */; }; + 6174D60C2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */; }; + 6174D60D2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */; }; + 6174D61D2C007B3300EC7469 /* ModuleName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D61C2C007B3300EC7469 /* ModuleName.swift */; }; + 6174D61E2C007B3300EC7469 /* ModuleName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D61C2C007B3300EC7469 /* ModuleName.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 */; }; @@ -423,6 +483,8 @@ 619CE75F2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */; }; 619CE7612A458D66005588CB /* TraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE7602A458D66005588CB /* TraceTests.swift */; }; 619CE7622A458D66005588CB /* TraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE7602A458D66005588CB /* TraceTests.swift */; }; + 619F5CEC2BF5089C004BFE70 /* GlobalRUMAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */; }; + 619F5CED2BF508A4004BFE70 /* GlobalRUMAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */; }; 61A1A44929643254007909E7 /* DatadogCoreProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */; }; 61A1A44A29643254007909E7 /* DatadogCoreProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */; }; 61A2CC212A443D330000FF25 /* DDRUMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */; }; @@ -489,6 +551,8 @@ 61C713D12A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */; }; 61C713D32A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */; }; 61C713D42A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */; }; + 61CE2E5F2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */; }; + 61CE2E602BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */; }; 61CE585A2B48174D00479510 /* SpanWriteContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE58592B48174D00479510 /* SpanWriteContext.swift */; }; 61CE585B2B48174D00479510 /* SpanWriteContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE58592B48174D00479510 /* SpanWriteContext.swift */; }; 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */; }; @@ -604,8 +668,8 @@ A7EA11622AB0CE6C00C73970 /* DDUIKitRUMActionsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */; }; A7EA88562B17639A00FE2580 /* ResourcesWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7EA88552B17639A00FE2580 /* ResourcesWriter.swift */; }; 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 */; }; + A7FA98CE2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */; }; + A7FA98CF2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.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 */; }; @@ -1948,6 +2012,7 @@ files = ( D240685927CF5D0100C04F44 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */, D240685527CF5D0100C04F44 /* DatadogCore.framework in ⚙️ Embed Framework Dependencies */, + 1434A4642B7F73170072E3BB /* OpenTelemetryApi.xcframework in ⚙️ Embed Framework Dependencies */, D24C9C4729A7A520002057CF /* DatadogLogs.framework in ⚙️ Embed Framework Dependencies */, D240686027CF5D0100C04F44 /* DatadogObjc.framework in ⚙️ Embed Framework Dependencies */, ); @@ -1962,6 +2027,7 @@ files = ( 3C2206F82AB9DBC600DE780C /* DatadogInternal.framework in Embed Frameworks */, 3C2206F72AB9DBB600DE780C /* DatadogTrace.framework in Embed Frameworks */, + 1434A4622B7F73110072E3BB /* OpenTelemetryApi.xcframework in Embed Frameworks */, 3C2206F62AB9DBA700DE780C /* DatadogRUM.framework in Embed Frameworks */, 3C2206F52AB9DB9000DE780C /* DatadogSessionReplay.framework in Embed Frameworks */, D240687E27CF982D00C04F44 /* DatadogCrashReporting.framework in Embed Frameworks */, @@ -1976,6 +2042,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugOTelTracingViewController.swift; sourceTree = ""; }; 3C0D5DD62A543B3B00446CF9 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 3C0D5DDC2A543D5D00446CF9 /* EventGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGenerator.swift; sourceTree = ""; }; 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventGeneratorTests.swift; sourceTree = ""; }; @@ -1984,18 +2051,38 @@ 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 = ""; }; + 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = OpenTelemetryApi.xcframework; path = ../Carthage/Build/OpenTelemetryApi.xcframework; sourceTree = ""; }; + 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLink.swift; sourceTree = ""; }; + 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLinkTests.swift; sourceTree = ""; }; + 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextMocks.swift; sourceTree = ""; }; + 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+Datadog.swift"; sourceTree = ""; }; + 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = ""; }; + 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanBuilder.swift; sourceTree = ""; }; + 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+Datadog.swift"; sourceTree = ""; }; + 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelSpanId+Datadog.swift"; sourceTree = ""; }; + 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelSpanId+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanTests.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 = ""; }; + 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceContextInjection.swift; sourceTree = ""; }; + 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TraceContextInjection+objc.swift"; sourceTree = ""; }; + 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NOPOTelSpan.swift; sourceTree = ""; }; + 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NOPOTelSpanBuilder.swift; sourceTree = ""; }; 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionInstrumentation.swift; sourceTree = ""; }; 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTask+Tracking.swift"; sourceTree = ""; }; + 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelAttributeValue+Datadog.swift"; sourceTree = ""; }; + 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelAttributeValue+DatadogTests.swift"; sourceTree = ""; }; 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionInstrumentation+objc.swift"; sourceTree = ""; }; 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDURLSessionInstrumentationConfigurationTests.swift; sourceTree = ""; }; 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDGeneratorTests.swift; sourceTree = ""; }; 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDTests.swift; sourceTree = ""; }; 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogWebViewTracking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogWebViewTrackingTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CF673352B4807490016CE17 /* OTelSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanTests.swift; sourceTree = ""; }; + 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelTracerProvider.swift; sourceTree = ""; }; 49274903288048AA00ECD49B /* InternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalProxyTests.swift; sourceTree = ""; }; 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMInternalProxyTests.swift; sourceTree = ""; }; 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUM+Internal.swift"; sourceTree = ""; }; @@ -2122,7 +2209,7 @@ 61054F902A6EE1BA00AAA894 /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; 61054F912A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentRequestBuilderTests.swift; sourceTree = ""; }; 61054F932A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTAssertRectsEqual.swift; sourceTree = ""; }; - 610ABD4B2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryCoreIntegrationTests.swift; sourceTree = ""; }; + 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTelemetryIntegrationTests.swift; sourceTree = ""; }; 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDRUM+apiTests.m"; sourceTree = ""; }; 6111C58125C0081F00F5C4A2 /* RUMDataModels+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMDataModels+objc.swift"; sourceTree = ""; }; 61122ECD25B1B74500F9C7F5 /* SpanSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanSanitizer.swift; sourceTree = ""; }; @@ -2183,6 +2270,7 @@ 611529A425E3DD51004F740E /* ValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisher.swift; sourceTree = ""; }; 611529AD25E3E429004F740E /* ValuePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisherTests.swift; sourceTree = ""; }; 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionDelegate+objc.swift"; sourceTree = ""; }; + 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorContextNotifierTests.swift; sourceTree = ""; }; 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; 61216275247D1CD700AC5D67 /* TracingWithLoggingIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingWithLoggingIntegration.swift; sourceTree = ""; }; 61216279247D21FE00AC5D67 /* TracingWithLoggingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingWithLoggingIntegrationTests.swift; sourceTree = ""; }; @@ -2214,7 +2302,7 @@ 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogSessionReplay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6133D2082A6EDB7700384BEF /* DatadogSessionReplayTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogSessionReplayTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 61345612244756E300E7DA6B /* PerformancePresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePresetTests.swift; sourceTree = ""; }; - 6134CDB02A691E850061CCD9 /* CoreMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMetricsTests.swift; sourceTree = ""; }; + 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchMetricsTests.swift; sourceTree = ""; }; 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDErrorTests.swift; sourceTree = ""; }; 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesOrchestrator+MetricsTests.swift"; sourceTree = ""; }; 61378BA72555329E00F28837 /* DatadogTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogTests.xcconfig; sourceTree = ""; }; @@ -2234,7 +2322,7 @@ 6141015A251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMUserActionsHandler.swift; sourceTree = ""; }; 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzlerTests.swift; sourceTree = ""; }; 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Casting+RUM.swift"; sourceTree = ""; }; - 614396712A67D74F00197326 /* CoreMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMetrics.swift; sourceTree = ""; }; + 614396712A67D74F00197326 /* BatchMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchMetrics.swift; sourceTree = ""; }; 61441C0224616DE9003D8BB8 /* Example iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppDelegate.swift; sourceTree = ""; }; 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Main iOS.storyboard"; sourceTree = ""; }; @@ -2252,6 +2340,7 @@ 61494CB024C839460082C633 /* RUMResourceScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMResourceScope.swift; sourceTree = ""; }; 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMResourceScopeTests.swift; sourceTree = ""; }; 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUserActionScope.swift; sourceTree = ""; }; + 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmutableRequest.swift; sourceTree = ""; }; 614B0A4A24EBC43D00A2A780 /* RUMUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUser.swift; sourceTree = ""; }; 614B0A4E24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMConnectivityInfoProvider.swift; sourceTree = ""; }; 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogCoreTests.swift; sourceTree = ""; }; @@ -2265,6 +2354,7 @@ 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 = ""; }; 61569894256D0E9A00C6AADA /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; + 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmutableRequestTests.swift; sourceTree = ""; }; 6156CB8D24DDA1B5008CB2B2 /* RUMContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMContextProvider.swift; sourceTree = ""; }; 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayDependencyTests.swift; sourceTree = ""; }; 615950ED291C058F00470E0C /* SessionReplayDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayDependency.swift; sourceTree = ""; }; @@ -2319,6 +2409,10 @@ 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 = ""; }; + 6174D6032BFB9AB600EC7469 /* WebViewTracking+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebViewTracking+objc.swift"; sourceTree = ""; }; + 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDWebViewTracking+apiTests.m"; sourceTree = ""; }; + 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKMetricFields.swift; sourceTree = ""; }; + 6174D61C2C007B3300EC7469 /* ModuleName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleName.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 = ""; }; @@ -2380,6 +2474,7 @@ 61993672265BC029009D7EA8 /* E2ETests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = E2ETests.xcconfig; sourceTree = ""; }; 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceConfigurationTests.swift; sourceTree = ""; }; 619CE7602A458D66005588CB /* TraceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceTests.swift; sourceTree = ""; }; + 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalRUMAttributes.swift; sourceTree = ""; }; 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogCoreProxy.swift; sourceTree = ""; }; 61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMConfigurationTests.swift; sourceTree = ""; }; 61A2CC232A44454D0000FF25 /* DDRUMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMTests.swift; sourceTree = ""; }; @@ -2450,6 +2545,7 @@ 61C713C92A3DC22700FA735A /* RUMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTests.swift; sourceTree = ""; }; 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureRegistrationCoreMock.swift; sourceTree = ""; }; 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyHelpers.swift; sourceTree = ""; }; + 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Monitor+GlobalAttributesTests.swift"; sourceTree = ""; }; 61CE58592B48174D00479510 /* SpanWriteContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanWriteContext.swift; sourceTree = ""; }; 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMDataModels+objcTests.swift"; sourceTree = ""; }; 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosInternetAddress.swift; sourceTree = ""; }; @@ -2587,7 +2683,7 @@ A7F773D32924EA2D00AC1A62 /* B3HTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = B3HTTPHeaders.swift; sourceTree = ""; }; A7F773DB29253F8B00AC1A62 /* B3HTTPHeadersWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersWriter.swift; sourceTree = ""; }; A7F773DC29253F8B00AC1A62 /* B3HTTPHeadersReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersReader.swift; sourceTree = ""; }; - A7FA98CD2BA1A6930018D6B5 /* SharedMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedMetrics.swift; sourceTree = ""; }; + A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodCalledMetric.swift; sourceTree = ""; }; B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReader.swift; sourceTree = ""; }; 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 = ""; }; @@ -2941,6 +3037,7 @@ D240687827CF982B00C04F44 /* CrashReporter.xcframework in Frameworks */, D240687B27CF982C00C04F44 /* DatadogCore.framework in Frameworks */, D240687D27CF982D00C04F44 /* DatadogCrashReporting.framework in Frameworks */, + 1434A4612B7F73110072E3BB /* OpenTelemetryApi.xcframework in Frameworks */, D24C9C4229A7A50D002057CF /* DatadogLogs.framework in Frameworks */, D240687F27CF982F00C04F44 /* DatadogObjc.framework in Frameworks */, ); @@ -3060,6 +3157,7 @@ buildActionMask = 2147483647; files = ( 61A2CC342A44A6030000FF25 /* DatadogRUM.framework in Frameworks */, + 1434A4632B7F73170072E3BB /* OpenTelemetryApi.xcframework in Frameworks */, D25CFA9D29C4FC6E00E3A43D /* DatadogTrace.framework in Frameworks */, 9E5BD8062819742C00CB568E /* SwiftUI.framework in Frameworks */, D240687027CF971C00C04F44 /* CrashReporter.xcframework in Frameworks */, @@ -3076,6 +3174,7 @@ files = ( D26F741129ACBDA100D25622 /* DatadogInternal.framework in Frameworks */, D2579592298ABCED008A1BE5 /* XCTest.framework in Frameworks */, + 3CDA3F7E2BCD866D005D2C13 /* DatadogSDKTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3085,6 +3184,7 @@ files = ( D26F741229ACBDAD00D25622 /* DatadogInternal.framework in Frameworks */, D230399E298D50F1001A1FA3 /* XCTest.framework in Frameworks */, + 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3092,6 +3192,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3C5D691F2B76825500C4E07E /* OpenTelemetryApi.xcframework in Frameworks */, D2C1A50E29C4C4EF00946C31 /* DatadogInternal.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3135,6 +3236,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3C5D69222B76826000C4E07E /* OpenTelemetryApi.xcframework in Frameworks */, D2C1A57429C4F30000946C31 /* DatadogInternal.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3228,9 +3330,40 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = { + isa = PBXGroup; + children = ( + 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */, + 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */, + 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */, + 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */, + 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */, + 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */, + 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */, + 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */, + 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */, + 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */, + ); + path = OpenTelemetry; + sourceTree = ""; + }; + 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */ = { + isa = PBXGroup; + children = ( + 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */, + 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */, + 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */, + 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */, + 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */, + 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */, + ); + path = OpenTelemetry; + sourceTree = ""; + }; 3CE11A3B29F7BEE700202522 /* DatadogWebViewTracking */ = { isa = PBXGroup; children = ( + 6174D6072BFCCDA400EC7469 /* ObjC */, 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */, D29732462A5C108700827599 /* DDScriptMessageHandler.swift */, D29732472A5C108700827599 /* MessageEmitter.swift */, @@ -3684,6 +3817,7 @@ 61054F7D2A6EE1BA00AAA894 /* Mocks */ = { isa = PBXGroup; children = ( + 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */, 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */, 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */, 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */, @@ -3692,7 +3826,6 @@ 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */, 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */, 61054F862A6EE1BA00AAA894 /* SnapshotProducerMocks.swift */, - 3C2B89412BE53D6200043847 /* RUMContextMocks.swift */, 61054F872A6EE1BA00AAA894 /* RUMContextObserverMock.swift */, A74A72862B10CE4100771FEB /* ResourceMocks.swift */, A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */, @@ -3765,7 +3898,7 @@ isa = PBXGroup; children = ( 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */, - 610ABD4B2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift */, + 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */, D20FD9D52ACC0934004D3569 /* WebLogIntegrationTests.swift */, D21831542B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift */, ); @@ -3899,6 +4032,7 @@ 61216277247D1F2100AC5D67 /* FeaturesIntegration */, 61133BB72423979B00786299 /* Utils */, 61D3E0C7277B237D008BE766 /* Kronos */, + 6174D6082BFDDD1E00EC7469 /* SDKMetrics */, ); name = DatadogCore; path = ../DatadogCore/Sources; @@ -3940,7 +4074,6 @@ 61C3638424361E9200C4D4E6 /* Globals.swift */, 6139CD702589FAFD007E8BB7 /* Retrying.swift */, 61DA8CAE28620C760074A606 /* Cryptography.swift */, - 614396712A67D74F00197326 /* CoreMetrics.swift */, ); path = Utils; sourceTree = ""; @@ -4063,6 +4196,7 @@ 61133C352423990D00786299 /* Utils */, 61BAD46826415FA2001886CA /* OpenTracing */, 61D3E0DD277B3D6E008BE766 /* Kronos */, + 6174D6092BFDDDE400EC7469 /* SDKMetrics */, ); path = Datadog; sourceTree = ""; @@ -4191,6 +4325,7 @@ 61133C6F2423993200786299 /* Frameworks */ = { isa = PBXGroup; children = ( + 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */, D2579591298ABCED008A1BE5 /* XCTest.framework */, D2579593298ABCF5008A1BE5 /* XCTest.framework */, 61B03ECC274FF00E00EB1AE1 /* SwiftUI.framework */, @@ -4284,6 +4419,7 @@ isa = PBXGroup; children = ( 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */, + 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */, 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */, A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */, A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */, @@ -4406,6 +4542,7 @@ children = ( 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */, 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */, + 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */, 617699202A8A7DF50030022B /* DebugManualTraceInjectionViewController.swift */, 61E5333724B84EE2003D6C4E /* DebugRUMViewController.swift */, 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */, @@ -4532,6 +4669,8 @@ 6167E6DF2B81203A00C3CA2D /* Models */ = { isa = PBXGroup; children = ( + 619F5CEB2BF5089B004BFE70 /* RUM */, + D25C834D2B88A261008E73B1 /* WebViewTracking */, 6167E6E02B81204B00C3CA2D /* CrashReporting */, ); path = Models; @@ -4636,6 +4775,39 @@ path = ../DatadogCrashReporting/Tests; sourceTree = ""; }; + 6174D6072BFCCDA400EC7469 /* ObjC */ = { + isa = PBXGroup; + children = ( + 6174D6032BFB9AB600EC7469 /* WebViewTracking+objc.swift */, + ); + path = ObjC; + sourceTree = ""; + }; + 6174D6082BFDDD1E00EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 614396712A67D74F00197326 /* BatchMetrics.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 6174D6092BFDDDE400EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 6174D60A2BFDDE1900EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */, + A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; 617699162A8608C20030022B /* Context */ = { isa = PBXGroup; children = ( @@ -4679,6 +4851,7 @@ 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */, 618715F624DC0CDE00FC0F69 /* RUMCommandTests.swift */, 6176C1712ABDBA2E00131A70 /* MonitorTests.swift */, + 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */, ); path = RUMMonitor; sourceTree = ""; @@ -4692,6 +4865,7 @@ 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */, 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */, 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */, + 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */, ); path = Scopes; sourceTree = ""; @@ -4747,7 +4921,6 @@ 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */, 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */, 61DA8CB1286215DE0074A606 /* CryptographyTests.swift */, - 6134CDB02A691E850061CCD9 /* CoreMetricsTests.swift */, ); path = Utils; sourceTree = ""; @@ -4845,6 +5018,14 @@ path = Reading; sourceTree = ""; }; + 619F5CEB2BF5089B004BFE70 /* RUM */ = { + isa = PBXGroup; + children = ( + 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */, + ); + path = RUM; + sourceTree = ""; + }; 61AE74112AD6EE7E008DB9BB /* Matchers */ = { isa = PBXGroup; children = ( @@ -4886,6 +5067,7 @@ A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */, 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */, A795069D2B974CAA00AC4814 /* DDSessionReplay+apiTests.m */, + 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */, ); path = ObjcAPITests; sourceTree = ""; @@ -4945,6 +5127,7 @@ 61C5A89724509C1100DA608C /* Tracing */ = { isa = PBXGroup; children = ( + 3CF673352B4807490016CE17 /* OTelSpanTests.swift */, 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */, D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */, D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */, @@ -5409,6 +5592,7 @@ D2160CC029C0DED100FAA9A5 /* URLSession */ = { isa = PBXGroup; children = ( + 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */, D295A16429F299C9007C0E9A /* URLSessionInterceptor.swift */, D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */, 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */, @@ -5439,14 +5623,6 @@ path = Swizzling; sourceTree = ""; }; - D21A94F02B838CCD00AC4256 /* Models */ = { - isa = PBXGroup; - children = ( - D25C834D2B88A261008E73B1 /* WebViewTracking */, - ); - path = Models; - sourceTree = ""; - }; D21AE6BA29E5ED7D0064BF29 /* Telemetry */ = { isa = PBXGroup; children = ( @@ -5494,8 +5670,8 @@ D23039D1298D5235001A1FA3 /* Upload */, D2A783D329A53049003B03BB /* Utils */, D2160CE229C0DFED00FAA9A5 /* Swizzling */, - D21A94F02B838CCD00AC4256 /* Models */, D2EBEE1D29BA15BC00B15732 /* NetworkInstrumentation */, + 6174D60A2BFDDE1900EC7469 /* SDKMetrics */, ); name = DatadogInternal; path = ../DatadogInternal/Sources; @@ -5727,6 +5903,7 @@ 61133C462423990D00786299 /* TestsDirectory.swift */, 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */, 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */, + 6174D61C2C007B3300EC7469 /* ModuleName.swift */, ); path = Helpers; sourceTree = ""; @@ -5742,6 +5919,7 @@ D25EE93529C4C3C300CE3839 /* DatadogTrace */ = { isa = PBXGroup; children = ( + 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */, 61A2CC382A44B0EA0000FF25 /* Trace.swift */, 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */, 61A2CC3B2A44BED30000FF25 /* Tracer.swift */, @@ -5764,6 +5942,7 @@ D25EE93F29C4C3C400CE3839 /* DatadogTraceTests */ = { isa = PBXGroup; children = ( + 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */, 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */, 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */, @@ -5951,7 +6130,6 @@ D23039DC298D5235001A1FA3 /* DDError.swift */, 61133BBA2423979B00786299 /* SwiftExtensions.swift */, D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */, - A7FA98CD2BA1A6930018D6B5 /* SharedMetrics.swift */, ); path = Utils; sourceTree = ""; @@ -6062,6 +6240,7 @@ D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */, D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */, 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */, + 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */, D2EBEDCC29B893D800B15732 /* TraceID.swift */, 3C9B27242B9F174700569C07 /* SpanID.swift */, D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */, @@ -6099,6 +6278,7 @@ D2BEEDAE2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift */, D2BEEDB72B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift */, D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */, + 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */, ); path = NetworkInstrumentation; sourceTree = ""; @@ -6698,6 +6878,9 @@ 3C41694229FBF6100042B9D2 /* PBXTargetDependency */, ); name = "TestUtilities iOS"; + packageProductDependencies = ( + 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */, + ); productName = TestUtilities; productReference = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; productType = "com.apple.product-type.framework"; @@ -6717,6 +6900,9 @@ D26F741529ACBDAD00D25622 /* PBXTargetDependency */, ); name = "TestUtilities tvOS"; + packageProductDependencies = ( + 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */, + ); productName = TestUtilities; productReference = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; productType = "com.apple.product-type.framework"; @@ -6736,6 +6922,8 @@ D2C1A51129C4C4EF00946C31 /* PBXTargetDependency */, ); name = "DatadogTrace iOS"; + packageProductDependencies = ( + ); productName = DatadogTrace; productReference = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; productType = "com.apple.product-type.framework"; @@ -6754,6 +6942,8 @@ D25EE93E29C4C3C300CE3839 /* PBXTargetDependency */, ); name = "DatadogTraceTests iOS"; + packageProductDependencies = ( + ); productName = DatadogTraceTests; productReference = D25EE93B29C4C3C300CE3839 /* DatadogTraceTests iOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -7104,6 +7294,9 @@ Base, ); mainGroup = 61133B78242393DE00786299; + packageReferences = ( + 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */, + ); productRefGroup = 61133B83242393DE00786299 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -7632,6 +7825,7 @@ files = ( 3C85D42129F7C5C900AFF894 /* WebViewTracking.swift in Sources */, D297324B2A5C108700827599 /* MessageEmitter.swift in Sources */, + 6174D6042BFB9AB600EC7469 /* WebViewTracking+objc.swift in Sources */, D29732492A5C108700827599 /* DDScriptMessageHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7678,7 +7872,7 @@ D20605CB2875A83F0047275C /* ContextValueReader.swift in Sources */, 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */, D20605A92874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */, - 614396722A67D74F00197326 /* CoreMetrics.swift in Sources */, + 614396722A67D74F00197326 /* BatchMetrics.swift in Sources */, D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */, 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */, 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */, @@ -7725,6 +7919,7 @@ D29A9FDA29DDC6D0005C54A4 /* RUMEventFileOutputTests.swift in Sources */, D28F836829C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */, 61B8BA91281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */, + 6174D6062BFB9D6400EC7469 /* DDWebViewTracking+apiTests.m in Sources */, 614798962A459AA80095CB02 /* DDTraceTests.swift in Sources */, D25085102976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */, 6167E6DD2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */, @@ -7830,7 +8025,7 @@ 6128F57B2BA35D6200D35B08 /* FeatureDataStoreTests.swift in Sources */, D29A9FCE29DDC4BA005C54A4 /* RUMFeatureMocks.swift in Sources */, 61133C5B2423990D00786299 /* DirectoryTests.swift in Sources */, - 610ABD4C2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift in Sources */, + 610ABD4C2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */, A79B0F64292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */, D2B3F052282E827700C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */, D20605B92875729E0047275C /* ContextValuePublisherMock.swift in Sources */, @@ -7841,13 +8036,14 @@ A7CA21802BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */, D20605C52875895E0047275C /* KronosClockMock.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, - 6134CDB12A691E850061CCD9 /* CoreMetricsTests.swift in Sources */, + 6134CDB12A691E850061CCD9 /* BatchMetricsTests.swift in Sources */, D24C9C6029A7CB0A002057CF /* DatadogLogsFeatureTests.swift in Sources */, 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */, D244B3A3271EDACD003E1B29 /* SwiftUIExtensionsTests.swift in Sources */, D24C9C6429A7CB7B002057CF /* CrashLogReceiverTests.swift in Sources */, 61B5E42126DF85C7000B0A5F /* DDRUMMonitor+apiTests.m in Sources */, 61133C4E2423990D00786299 /* UIKitMocks.swift in Sources */, + 3CF673362B4807490016CE17 /* OTelSpanTests.swift in Sources */, 61F3E36D2BC7D66700C7881E /* HeadBasedSamplingTests.swift in Sources */, D20FD9D32ACC08D1004D3569 /* WebKitMocks.swift in Sources */, 618353BC2A69470A0085F84A /* CoreMetricsIntegrationTests.swift in Sources */, @@ -7896,6 +8092,7 @@ A79B0F66292BD7CA008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */, 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, 61133C0E2423983800786299 /* Datadog+objc.swift in Sources */, + 3CA852642BF2148200B52CBA /* TraceContextInjection+objc.swift in Sources */, 61133C102423983800786299 /* Logs+objc.swift in Sources */, 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */, 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */, @@ -8066,7 +8263,7 @@ 61054F992A6EE1BA00AAA894 /* ColorsTests.swift in Sources */, 61054FBF2A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift in Sources */, 61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift in Sources */, - 3C2B89422BE53D6200043847 /* RUMContextMocks.swift in Sources */, + 3C33E4072BEE35A8003B2988 /* RUMContextMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8079,6 +8276,7 @@ 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */, 618236892710560900125326 /* DebugWebviewViewController.swift in Sources */, 61F74AF426F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift in Sources */, + 1434A4662B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */, 61E5333824B84EE2003D6C4E /* DebugRUMViewController.swift in Sources */, 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */, 61020C2C2758E853005EEAEA /* DebugBackgroundEventsViewController.swift in Sources */, @@ -8244,6 +8442,7 @@ D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */, D2EBEE2629BA160F00B15732 /* B3HTTPHeaders.swift in Sources */, D23354FC2A42E32000AFCAE2 /* InternalExtended.swift in Sources */, + 619F5CEC2BF5089C004BFE70 /* GlobalRUMAttributes.swift in Sources */, D23039F3298D5236001A1FA3 /* DynamicCodingKey.swift in Sources */, D2BEEDB22B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift in Sources */, D23039FE298D5236001A1FA3 /* FeatureRequestBuilder.swift in Sources */, @@ -8279,6 +8478,7 @@ D23039F5298D5236001A1FA3 /* AnyEncodable.swift in Sources */, D2303A00298D5236001A1FA3 /* DatadogExtended.swift in Sources */, D23039E6298D5236001A1FA3 /* Sysctl.swift in Sources */, + 614A708E2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, D2160CF429C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, D23039E1298D5236001A1FA3 /* AppState.swift in Sources */, D2DE63532A30A7CA00441A54 /* CoreRegistry.swift in Sources */, @@ -8298,7 +8498,7 @@ D2EBEE2229BA160F00B15732 /* TracePropagationHeadersReader.swift in Sources */, D2303A02298D5236001A1FA3 /* ReadWriteLock.swift in Sources */, D2EBEE2429BA160F00B15732 /* W3CHTTPHeadersReader.swift in Sources */, - A7FA98CE2BA1A6930018D6B5 /* SharedMetrics.swift in Sources */, + A7FA98CE2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */, D23039E8298D5236001A1FA3 /* DatadogContext.swift in Sources */, D23039FF298D5236001A1FA3 /* Foundation+Datadog.swift in Sources */, D2F8235329915E12003C7E99 /* DatadogSite.swift in Sources */, @@ -8308,6 +8508,7 @@ D2EBEE2729BA160F00B15732 /* B3HTTPHeadersWriter.swift in Sources */, D23039E2298D5236001A1FA3 /* UserInfo.swift in Sources */, D23039FB298D5236001A1FA3 /* URLRequestBuilder.swift in Sources */, + 3CA8525F2BF2073800B52CBA /* TraceContextInjection.swift in Sources */, D23039F6298D5236001A1FA3 /* Attributes.swift in Sources */, D20731CB29A52E6000ECBF94 /* Sampler.swift in Sources */, D2EBEE2029BA160F00B15732 /* TracePropagationHeadersWriter.swift in Sources */, @@ -8320,6 +8521,7 @@ D23039E0298D5235001A1FA3 /* DatadogCoreProtocol.swift in Sources */, D23039FD298D5236001A1FA3 /* DataCompression.swift in Sources */, 6167E6F92B81E95900C3CA2D /* BinaryImage.swift in Sources */, + 6174D60C2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */, D23039F0298D5236001A1FA3 /* AnyEncoder.swift in Sources */, D2A783D429A5309F003B03BB /* SwiftExtensions.swift in Sources */, 3C0D5DD72A543B3B00446CF9 /* Event.swift in Sources */, @@ -8428,6 +8630,7 @@ D23F8EAB29DDCD38001CFAE8 /* RUMDataModelMocks.swift in Sources */, D23F8EAC29DDCD38001CFAE8 /* RUMDataModelsMappingTests.swift in Sources */, D23F8EAD29DDCD38001CFAE8 /* RUMEventBuilderTests.swift in Sources */, + 61CE2E602BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */, D23F8EAE29DDCD38001CFAE8 /* DDTAssertValidRUMUUID.swift in Sources */, D23F8EAF29DDCD38001CFAE8 /* RUMScopeTests.swift in Sources */, D23F8EB029DDCD38001CFAE8 /* SessionReplayDependencyTests.swift in Sources */, @@ -8435,6 +8638,7 @@ D23F8EB129DDCD38001CFAE8 /* RUMViewScopeTests.swift in Sources */, D224431029E977A100274EC7 /* TelemetryReceiverTests.swift in Sources */, D23F8EB229DDCD38001CFAE8 /* ValuePublisherTests.swift in Sources */, + 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, 61C713BD2A3C95AD00FA735A /* RUMInstrumentationTests.swift in Sources */, D23F8EB329DDCD38001CFAE8 /* ErrorMessageReceiverTests.swift in Sources */, 61C713C12A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */, @@ -8461,6 +8665,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1434A4672B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */, D2F44FC3299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift in Sources */, D240680827CE6C9E00C04F44 /* ConsoleOutputInterceptor.swift in Sources */, D240681E27CE6C9E00C04F44 /* ExampleAppDelegate.swift in Sources */, @@ -8496,6 +8701,7 @@ D2A7840329A536AD003B03BB /* PrintFunctionMock.swift in Sources */, D2A7840D29A53A4B003B03BB /* TestsDirectory.swift in Sources */, D2DA23CF298D5F2300C6C7E6 /* FeatureMessageReceiverMock.swift in Sources */, + 6174D61D2C007B3300EC7469 /* ModuleName.swift in Sources */, D2160CF029C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift in Sources */, D2579554298ABB04008A1BE5 /* FeatureBaggageMock.swift in Sources */, 3C85D42C29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */, @@ -8541,6 +8747,7 @@ D2A7840429A536AD003B03BB /* PrintFunctionMock.swift in Sources */, D2A7840E29A53A4B003B03BB /* TestsDirectory.swift in Sources */, D2DA23D0298D5F2300C6C7E6 /* FeatureMessageReceiverMock.swift in Sources */, + 6174D61E2C007B3300EC7469 /* ModuleName.swift in Sources */, D2160CF129C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift in Sources */, D257957E298ABB83008A1BE5 /* FeatureBaggageMock.swift in Sources */, 3C85D42D29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */, @@ -8564,20 +8771,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */, 61A2CC3C2A44BED30000FF25 /* Tracer.swift in Sources */, D2C1A50229C4C4CB00946C31 /* Casting.swift in Sources */, + 3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */, D2C1A50C29C4C4CB00946C31 /* DDNoOps.swift in Sources */, D2C1A4FC29C4C4CB00946C31 /* RequestBuilder.swift in Sources */, D2C1A50D29C4C4CB00946C31 /* SpanTagsReducer.swift in Sources */, + 3CB012DD2B482E0400557951 /* NOPOTelSpan.swift in Sources */, 61CE585A2B48174D00479510 /* SpanWriteContext.swift in Sources */, D2C1A51829C4C53F00946C31 /* OTSpan.swift in Sources */, D2C1A51429C4C53F00946C31 /* OTSpanContext.swift in Sources */, + 3C6C7FEB2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */, + 3C6C7FE92B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */, D2C1A51329C4C53F00946C31 /* OTReference.swift in Sources */, + 3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */, D2C1A4FB29C4C4CB00946C31 /* MessageReceivers.swift in Sources */, + 3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */, 61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */, 61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */, D2C1A50029C4C4CB00946C31 /* ActiveSpansPool.swift in Sources */, + 3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */, D2C1A50929C4C4CB00946C31 /* SpanEventEncoder.swift in Sources */, + 3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */, D2C1A4FE29C4C4CB00946C31 /* SpanEventMapper.swift in Sources */, D2C1A50329C4C4CB00946C31 /* DDFormat.swift in Sources */, D2C1A51629C4C53F00946C31 /* OTConstants.swift in Sources */, @@ -8590,6 +8806,7 @@ D2C1A4FF29C4C4CB00946C31 /* Warnings.swift in Sources */, D2C1A51729C4C53F00946C31 /* OTFormat.swift in Sources */, D22C5BCB2A98A5400024CC1F /* Baggages.swift in Sources */, + 3CFF5D492B555F4F00FC483A /* OTelTracerProvider.swift in Sources */, D2C1A4FA29C4C4CB00946C31 /* SpanSanitizer.swift in Sources */, D2C1A50A29C4C4CB00946C31 /* TraceFeature.swift in Sources */, D2C1A50829C4C4CB00946C31 /* TracingURLSessionHandler.swift in Sources */, @@ -8602,6 +8819,7 @@ buildActionMask = 2147483647; files = ( D2C1A51E29C4C75700946C31 /* Casting+Tracing.swift in Sources */, + 3CC6AD1D2B4F07FA00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */, D2C1A52429C4C75700946C31 /* TracingURLSessionHandlerTests.swift in Sources */, 619CE75E2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, D2C1A52329C4C75700946C31 /* WarningsTests.swift in Sources */, @@ -8610,11 +8828,16 @@ D2C1A52229C4C75700946C31 /* DDNoopTracerTests.swift in Sources */, 61F3E3632BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */, + 3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */, 619CE7612A458D66005588CB /* TraceTests.swift in Sources */, 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */, + 3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */, + 3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */, + 3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */, D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */, D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */, + 3C6C7FFB2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */, D2C1A51F29C4C75700946C31 /* ActiveSpansPoolTests.swift in Sources */, D2C1A52529C4C75700946C31 /* SpanSanitizerTests.swift in Sources */, ); @@ -8719,6 +8942,7 @@ D29A9FC629DDBA8A005C54A4 /* RUMDataModelMocks.swift in Sources */, D29A9FD529DDC624005C54A4 /* RUMDataModelsMappingTests.swift in Sources */, D29A9FBE29DDB483005C54A4 /* RUMEventBuilderTests.swift in Sources */, + 61CE2E5F2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */, D29A9FCC29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift in Sources */, D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */, D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */, @@ -8726,6 +8950,7 @@ D29A9FB829DDB483005C54A4 /* RUMViewScopeTests.swift in Sources */, D224430F29E9779F00274EC7 /* TelemetryReceiverTests.swift in Sources */, D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */, + 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, 61C713BC2A3C95AD00FA735A /* RUMInstrumentationTests.swift in Sources */, D29A9FBB29DDB483005C54A4 /* ErrorMessageReceiverTests.swift in Sources */, 61C713C02A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */, @@ -8768,20 +8993,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */, 61A2CC3D2A44BED30000FF25 /* Tracer.swift in Sources */, D2C1A53829C4F2DF00946C31 /* Casting.swift in Sources */, + 3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */, D2C1A53929C4F2DF00946C31 /* DDNoOps.swift in Sources */, D2C1A53A29C4F2DF00946C31 /* RequestBuilder.swift in Sources */, D2C1A53B29C4F2DF00946C31 /* SpanTagsReducer.swift in Sources */, + 3CB012DE2B482E0400557951 /* NOPOTelSpan.swift in Sources */, 61CE585B2B48174D00479510 /* SpanWriteContext.swift in Sources */, D2C1A53D29C4F2DF00946C31 /* OTSpan.swift in Sources */, D2C1A53E29C4F2DF00946C31 /* OTSpanContext.swift in Sources */, + 3C6C7FEC2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */, + 3C6C7FEA2B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */, D2C1A53F29C4F2DF00946C31 /* OTReference.swift in Sources */, + 3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */, D2C1A54129C4F2DF00946C31 /* MessageReceivers.swift in Sources */, + 3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */, 61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */, 61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */, D2C1A54229C4F2DF00946C31 /* ActiveSpansPool.swift in Sources */, + 3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */, D2C1A54329C4F2DF00946C31 /* SpanEventEncoder.swift in Sources */, + 3C6C7FE82B459AAA006F5CBC /* OTelSpan.swift in Sources */, D2C1A54429C4F2DF00946C31 /* SpanEventMapper.swift in Sources */, D2C1A54529C4F2DF00946C31 /* DDFormat.swift in Sources */, D2C1A54629C4F2DF00946C31 /* OTConstants.swift in Sources */, @@ -8794,6 +9028,7 @@ D2C1A54D29C4F2DF00946C31 /* Warnings.swift in Sources */, D2C1A54E29C4F2DF00946C31 /* OTFormat.swift in Sources */, D22C5BCC2A98A5400024CC1F /* Baggages.swift in Sources */, + 3CFF5D4A2B555F4F00FC483A /* OTelTracerProvider.swift in Sources */, D2C1A54F29C4F2DF00946C31 /* SpanSanitizer.swift in Sources */, D2C1A55029C4F2DF00946C31 /* TraceFeature.swift in Sources */, D2C1A55129C4F2DF00946C31 /* TracingURLSessionHandler.swift in Sources */, @@ -8806,6 +9041,7 @@ buildActionMask = 2147483647; files = ( D2C1A55F29C4F2E800946C31 /* Casting+Tracing.swift in Sources */, + 3CC6AD1E2B4F07FB00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */, D2C1A56029C4F2E800946C31 /* TracingURLSessionHandlerTests.swift in Sources */, 619CE75F2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, D2C1A56129C4F2E800946C31 /* WarningsTests.swift in Sources */, @@ -8814,11 +9050,16 @@ D2C1A56329C4F2E800946C31 /* DDNoopTracerTests.swift in Sources */, 61F3E3642BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */, + 3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */, 619CE7622A458D66005588CB /* TraceTests.swift in Sources */, 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */, + 3C5D636D2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */, + 3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */, + 3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */, D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */, D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */, + 3C6C7FFE2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */, D2C1A56929C4F2E800946C31 /* ActiveSpansPoolTests.swift in Sources */, D2C1A56A29C4F2E800946C31 /* SpanSanitizerTests.swift in Sources */, ); @@ -8856,7 +9097,7 @@ D2A7841029A53B2F003B03BB /* Directory.swift in Sources */, 61DA8CB028620C760074A606 /* Cryptography.swift in Sources */, D2DC4BF727F484AA00E4FB96 /* DataEncryption.swift in Sources */, - 614396732A67D74F00197326 /* CoreMetrics.swift in Sources */, + 614396732A67D74F00197326 /* BatchMetrics.swift in Sources */, D29294E1291D5ED500F8EFF9 /* ApplicationVersionPublisher.swift in Sources */, D20605A7287476230047275C /* ServerOffsetPublisher.swift in Sources */, D21C26C628A3B49C005DD405 /* FeatureStorage.swift in Sources */, @@ -8903,7 +9144,7 @@ D28F836929C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */, 61B8BA92281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */, 3C1890162ABDE9C000CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */, - 6134CDB22A691E850061CCD9 /* CoreMetricsTests.swift in Sources */, + 6134CDB22A691E850061CCD9 /* BatchMetricsTests.swift in Sources */, 61F930C62BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */, D22743E029DEB8B5001A7EF9 /* VitalCPUReaderTests.swift in Sources */, D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */, @@ -8919,7 +9160,7 @@ D2CB6EE527C520D400A62B57 /* DataUploadConditionsTests.swift in Sources */, D2CB6EE627C520D400A62B57 /* DateFormattingTests.swift in Sources */, D2CB6EE727C520D400A62B57 /* FileTests.swift in Sources */, - 610ABD4D2A6930CA00AFEA34 /* TelemetryCoreIntegrationTests.swift in Sources */, + 610ABD4D2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */, D2CB6EEA27C520D400A62B57 /* LogMatcher.swift in Sources */, D2CB6EEC27C520D400A62B57 /* CustomObjcViewController.m in Sources */, D2EFA876286E011900F1FAA6 /* DatadogContextProviderTests.swift in Sources */, @@ -9011,6 +9252,7 @@ D2B3F053282E827B00C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */, D20605BA2875729E0047275C /* ContextValuePublisherMock.swift in Sources */, D22743E729DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */, + 3CF673372B4807490016CE17 /* OTelSpanTests.swift in Sources */, D25CFAA029C860E300E3A43D /* TracingFeatureMocks.swift in Sources */, D2CB6F5F27C520D400A62B57 /* DDNSURLSessionDelegate+apiTests.m in Sources */, 6128F58B2BA9860B00D35B08 /* DataStoreFileReaderTests.swift in Sources */, @@ -9074,6 +9316,7 @@ D2CB6FA127C5217A00A62B57 /* HTTPHeadersWriter+objc.swift in Sources */, 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, D2CB6FA227C5217A00A62B57 /* DDSpan+objc.swift in Sources */, + 3CA852652BF2148400B52CBA /* TraceContextInjection+objc.swift in Sources */, D2CB6FA327C5217A00A62B57 /* OTSpan+objc.swift in Sources */, D2CB6FA427C5217A00A62B57 /* DDURLSessionDelegate+objc.swift in Sources */, D2CB6FA527C5217A00A62B57 /* RUM+objc.swift in Sources */, @@ -9131,6 +9374,7 @@ D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */, D2EBEE3429BA161100B15732 /* B3HTTPHeaders.swift in Sources */, D23354FD2A42E32000AFCAE2 /* InternalExtended.swift in Sources */, + 619F5CED2BF508A4004BFE70 /* GlobalRUMAttributes.swift in Sources */, D2DA235B298D57AA00C6C7E6 /* DynamicCodingKey.swift in Sources */, D2BEEDB32B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift in Sources */, D2DA235C298D57AA00C6C7E6 /* FeatureRequestBuilder.swift in Sources */, @@ -9166,6 +9410,7 @@ D2DA2369298D57AA00C6C7E6 /* AnyEncodable.swift in Sources */, D2DA236A298D57AA00C6C7E6 /* DatadogExtended.swift in Sources */, D2DA236B298D57AA00C6C7E6 /* Sysctl.swift in Sources */, + 614A708F2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, D2160CF529C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, D2DA236C298D57AA00C6C7E6 /* AppState.swift in Sources */, D2DE63542A30A7CA00441A54 /* CoreRegistry.swift in Sources */, @@ -9185,7 +9430,7 @@ D2EBEE3029BA161100B15732 /* TracePropagationHeadersReader.swift in Sources */, D2DA2373298D57AA00C6C7E6 /* ReadWriteLock.swift in Sources */, D2EBEE3229BA161100B15732 /* W3CHTTPHeadersReader.swift in Sources */, - A7FA98CF2BA1A6930018D6B5 /* SharedMetrics.swift in Sources */, + A7FA98CF2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */, D2DA2374298D57AA00C6C7E6 /* DatadogContext.swift in Sources */, D2DA2375298D57AA00C6C7E6 /* Foundation+Datadog.swift in Sources */, D2F8235429915E12003C7E99 /* DatadogSite.swift in Sources */, @@ -9195,6 +9440,7 @@ D2EBEE3529BA161100B15732 /* B3HTTPHeadersWriter.swift in Sources */, D2DA2376298D57AA00C6C7E6 /* UserInfo.swift in Sources */, D2DA2377298D57AA00C6C7E6 /* URLRequestBuilder.swift in Sources */, + 3CA852602BF2073800B52CBA /* TraceContextInjection.swift in Sources */, D2DA2378298D57AA00C6C7E6 /* Attributes.swift in Sources */, D20731CC29A52E6000ECBF94 /* Sampler.swift in Sources */, D2EBEE2E29BA161100B15732 /* TracePropagationHeadersWriter.swift in Sources */, @@ -9207,6 +9453,7 @@ D2DA237C298D57AA00C6C7E6 /* DatadogCoreProtocol.swift in Sources */, D2DA237D298D57AA00C6C7E6 /* DataCompression.swift in Sources */, 6167E6FA2B81E95900C3CA2D /* BinaryImage.swift in Sources */, + 6174D60D2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */, D2DA237E298D57AA00C6C7E6 /* AnyEncoder.swift in Sources */, D2A783D529A530A0003B03BB /* SwiftExtensions.swift in Sources */, 3C0D5DD82A543B3B00446CF9 /* Event.swift in Sources */, @@ -9248,6 +9495,7 @@ D2216EC32A96649500ADAEC8 /* FeatureBaggageTests.swift in Sources */, D2160CDC29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, + 6156A9072BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */, D2F44FB8299AA1DA0074B0D9 /* DataCompressionTests.swift in Sources */, D2160CE029C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, D2EBEE3B29BA163E00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, @@ -9295,6 +9543,7 @@ D2216EC42A96649700ADAEC8 /* FeatureBaggageTests.swift in Sources */, D2160CDD29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, + 6156A9082BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */, D2F44FB9299AA1DB0074B0D9 /* DataCompressionTests.swift in Sources */, D2160CE129C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, D2EBEE3F29BA163F00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, @@ -13332,6 +13581,30 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/DataDog/dd-sdk-swift-testing.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */ = { + isa = XCSwiftPackageProductDependency; + package = 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */; + productName = DatadogSDKTesting; + }; + 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */ = { + isa = XCSwiftPackageProductDependency; + package = 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */; + productName = DatadogSDKTesting; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 61133B79242393DE00786299 /* Project object */; } diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index 3992068e2e..20bcde93a6 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -196,20 +196,6 @@ BlueprintName = "DatadogCoreTests iOS" ReferencedContainer = "container:Datadog.xcodeproj"> - - - - - - - - - - diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme index d512262352..b30d9a0578 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme @@ -182,26 +182,6 @@ BlueprintName = "DatadogCoreTests tvOS" ReferencedContainer = "container:Datadog.xcodeproj"> - - - - - - - - - - - - - - diff --git a/Datadog/E2ETests/Tracing/TracerE2ETests.swift b/Datadog/E2ETests/Tracing/TracerE2ETests.swift index c90a2176f4..f56bc03e76 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(samplingStrategy: .custom(sampleRate: 20)) + let anyWriter = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 20), traceContextInjection: .all) measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { tracer.inject(spanContext: anySpan.context, writer: anyWriter) diff --git a/Datadog/Example/Base.lproj/Main iOS.storyboard b/Datadog/Example/Base.lproj/Main iOS.storyboard index d4a0c551e4..fb7c848fb2 100644 --- a/Datadog/Example/Base.lproj/Main iOS.storyboard +++ b/Datadog/Example/Base.lproj/Main iOS.storyboard @@ -1,9 +1,9 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -65,9 +65,29 @@ - + + + + + + + + + + + + + + + @@ -86,7 +106,7 @@ - + @@ -106,7 +126,7 @@ - + @@ -126,7 +146,7 @@ - + @@ -146,7 +166,7 @@ - + @@ -166,7 +186,7 @@ - + @@ -784,7 +804,359 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -795,7 +1167,7 @@ - + @@ -944,11 +1316,57 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift b/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift index 407866e60c..8c531f96ff 100644 --- a/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift +++ b/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift @@ -47,4 +47,22 @@ class DebugCrashReportingWithRUMViewController: UIViewController { self?.crash() } } + + // MARK: - OOM Crash + + @IBAction func didTapOOMCrash(_ sender: UIButton) { + DispatchQueue.main.async { + let megaByte = 1_024 * 1_024 + let memoryPageSize = NSPageSize() + let memoryPages = megaByte / memoryPageSize + + while true { + // Allocate one MB and set one element of each memory page to something. + let ptr = UnsafeMutablePointer.allocate(capacity: megaByte) + for i in 0.. "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", diff --git a/DatadogCore.podspec b/DatadogCore.podspec index bc4dc1562d..db695ade56 100644 --- a/DatadogCore.podspec +++ b/DatadogCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCore" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogCore/Private/ObjcAppLaunchHandler.m b/DatadogCore/Private/ObjcAppLaunchHandler.m index 1b2a2dd2dd..3239d61391 100644 --- a/DatadogCore/Private/ObjcAppLaunchHandler.m +++ b/DatadogCore/Private/ObjcAppLaunchHandler.m @@ -127,7 +127,8 @@ int processStartTimeIntervalSinceReferenceDate(NSTimeInterval *timeInterval) { // The process' start time is provided relative to 1 Jan 1970 struct timeval startTime = kip.kp_proc.p_starttime; - NSTimeInterval processStartTime = startTime.tv_sec + startTime.tv_usec / USEC_PER_SEC; + // Multiplication with 1.0 ensure we don't round to 0 with integer division + NSTimeInterval processStartTime = startTime.tv_sec + (1.0 * startTime.tv_usec) / USEC_PER_SEC; // Convert to time since 1 Jan 2001 to align with CFAbsoluteTimeGetCurrent() *timeInterval = processStartTime - kCFAbsoluteTimeIntervalSince1970; return res; diff --git a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift index 8f587a376f..52be47d9df 100644 --- a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -33,6 +33,9 @@ internal class FilesOrchestrator: FilesOrchestratorType { /// Tracks the size of last writable file by accumulating the total `writeSize:` requested in `getWritableFile(writeSize:)` /// This is approximated value as it assumes that all requested writes succed. The actual difference should be negligible. private var lastWritableFileApproximatedSize: UInt64 = 0 + /// Tracks the last date when writtable file was requested for write operation. + /// It is used to compute the "batch duration" from the moment file was created to the moment it was last written. + private var lastWritableFileLastWriteDate: Date? = nil /// Telemetry interface. let telemetry: Telemetry @@ -75,6 +78,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { if let lastWritableFile = reuseLastWritableFileIfPossible(writeSize: writeSize) { // if last writable file can be reused lastWritableFileObjectsCount += 1 lastWritableFileApproximatedSize += writeSize + lastWritableFileLastWriteDate = dateProvider.now return lastWritableFile } else { if let closedBatchName = lastWritableFileName { @@ -103,6 +107,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { lastWritableFileName = newFile.name lastWritableFileObjectsCount = 1 lastWritableFileApproximatedSize = writeSize + lastWritableFileLastWriteDate = dateProvider.now return newFile } @@ -229,7 +234,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { telemetry.metric( name: BatchDeletedMetric.name, attributes: [ - BasicMetric.typeKey: BatchDeletedMetric.typeValue, + SDKMetricFields.typeKey: BatchDeletedMetric.typeValue, BatchMetric.trackKey: metricsData.trackName, BatchDeletedMetric.uploaderDelayKey: [ BatchDeletedMetric.uploaderDelayMinKey: metricsData.uploaderPerformance.minUploadDelay.toMilliseconds, @@ -251,13 +256,15 @@ internal class FilesOrchestrator: FilesOrchestratorType { guard let metricsData = metricsData else { return // do not track metrics for this orchestrator } - - let batchDuration = dateProvider.now.timeIntervalSince(fileCreationDateFrom(fileName: fileName)) + guard let lastWriteDate = lastWritableFileLastWriteDate else { + return // not reachable + } + let batchDuration = lastWriteDate.timeIntervalSince(fileCreationDateFrom(fileName: fileName)) telemetry.metric( name: BatchClosedMetric.name, attributes: [ - BasicMetric.typeKey: BatchClosedMetric.typeValue, + SDKMetricFields.typeKey: BatchClosedMetric.typeValue, BatchMetric.trackKey: metricsData.trackName, BatchMetric.consentKey: metricsData.consentLabel, BatchClosedMetric.uploaderWindowKey: performance.uploaderWindow.toMilliseconds, diff --git a/DatadogCore/Sources/Core/Upload/URLSessionClient.swift b/DatadogCore/Sources/Core/Upload/URLSessionClient.swift index 8725d896eb..1cf66dd5d9 100644 --- a/DatadogCore/Sources/Core/Upload/URLSessionClient.swift +++ b/DatadogCore/Sources/Core/Upload/URLSessionClient.swift @@ -20,10 +20,8 @@ internal class URLSessionClient: HTTPClient { // URLSession does not set the `Proxy-Authorization` header automatically when using a proxy // configuration. We manually set the HTTP basic authentication header. - if - let user = proxyConfiguration?[kCFProxyUsernameKey] as? String, - let password = proxyConfiguration?[kCFProxyPasswordKey] as? String - { + if let user = proxyConfiguration?[kCFProxyUsernameKey] as? String, + let password = proxyConfiguration?[kCFProxyPasswordKey] as? String { let authorization = basicHTTPAuthentication(username: user, password: password) configuration.httpAdditionalHeaders = ["Proxy-Authorization": authorization] } diff --git a/DatadogCore/Sources/Datadog+Internal.swift b/DatadogCore/Sources/Datadog+Internal.swift index 4b8175faa1..ecf30fd50e 100644 --- a/DatadogCore/Sources/Datadog+Internal.swift +++ b/DatadogCore/Sources/Datadog+Internal.swift @@ -43,7 +43,7 @@ public struct _TelemetryProxy { /// See Telementry.error public func error(id: String, message: String, kind: String?, stack: String?) { - telemetry.error(id: id, message: message, kind: kind, stack: stack) + telemetry.error(id: id, message: message, kind: kind ?? "unknown", stack: stack ?? "unknown") } } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index d7f32e57b4..1c9f2f77b0 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -474,7 +474,7 @@ public enum Datadog { core.telemetry.configuration( backgroundTasksEnabled: configuration.backgroundTasksEnabled, batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), - batchSize: Int64(exactly: performance.maxFileSize), + batchSize: performance.uploaderWindow.toInt64Milliseconds, batchUploadFrequency: performance.minUploadDelay.toInt64Milliseconds, useLocalEncryption: configuration.encryption != nil, useProxy: configuration.proxyConfiguration != nil diff --git a/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.swift b/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.swift index b3a6075d74..ea08775908 100644 --- a/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.swift +++ b/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.swift @@ -20,12 +20,15 @@ internal class CITestIntegration { let testExecutionId: String /// Tag that must be added to spans and headers when running inside a CIApp test let origin = "ciapp-test" + // UUID for test process message channel + private let messageChannelUUID: String? private init?(processInfo: ProcessInfo = .processInfo) { guard let testID = processInfo.environment["CI_VISIBILITY_TEST_EXECUTION_ID"] else { return nil } self.testExecutionId = testID + self.messageChannelUUID = processInfo.environment["CI_VISIBILITY_MESSAGE_CHANNEL_UUID"] } /// Entry point for running all the tasks needed for CIApp integration @@ -38,7 +41,10 @@ internal class CITestIntegration { /// created in the CIApp framework private func notifyRUMSession() { let timeout: CFTimeInterval = 1.0 - guard let remotePort = CFMessagePortCreateRemote(nil, "DatadogTestingPort" as CFString) else { + + guard let remotePort = CFMessagePortCreateRemote( + nil, messagePortId(name: "DatadogTestingPort") + ) else { return } CFMessagePortSendRequest( @@ -65,10 +71,18 @@ internal class CITestIntegration { return nil } - guard let port = CFMessagePortCreateLocal(nil, "DatadogRUMTestingPort" as CFString, attributeCallback, nil, nil) else { + guard let port = CFMessagePortCreateLocal( + nil, messagePortId(name: "DatadogRUMTestingPort"), attributeCallback, nil, nil + ) else { return } let runLoopSource = CFMessagePortCreateRunLoopSource(nil, port, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes) } + + /// Creates a ID for message port. If UUID is provided joins name with UUID. + /// Fallbacks to name if UUID is nil for backward compatibility + private func messagePortId(name: String) -> CFString { + (messageChannelUUID.map { "\(name)-\($0)" } ?? name) as CFString + } } diff --git a/DatadogCore/Sources/Kronos/KronosNTPClient.swift b/DatadogCore/Sources/Kronos/KronosNTPClient.swift index 757459254c..17e9323c3e 100644 --- a/DatadogCore/Sources/Kronos/KronosNTPClient.swift +++ b/DatadogCore/Sources/Kronos/KronosNTPClient.swift @@ -44,7 +44,7 @@ internal final class KronosNTPClient { var servers: [KronosInternetAddress: [KronosNTPPacket]] = [:] var completed: Int = 0 - let queryIPAndStoreResult = { (address: KronosInternetAddress, totalQueries: Int) -> Void in + let queryIPAndStoreResult = { (address: KronosInternetAddress, totalQueries: Int) in self.query(ip: address, port: port, version: version, timeout: timeout, numberOfSamples: numberOfSamples) { packet in defer { completed += 1 @@ -106,8 +106,8 @@ internal final class KronosNTPClient { timer?.invalidate() guard let data = data, let PDU = try? KronosNTPPacket(data: data, destinationTime: destinationTime), - PDU.isValidResponse() else - { + PDU.isValidResponse() + else { completion(nil) return } diff --git a/DatadogCore/Sources/Utils/CoreMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift similarity index 100% rename from DatadogCore/Sources/Utils/CoreMetrics.swift rename to DatadogCore/Sources/SDKMetrics/BatchMetrics.swift diff --git a/DatadogCore/Sources/Versioning.swift b/DatadogCore/Sources/Versioning.swift index a19f9e6a0f..faeed4bc02 100644 --- a/DatadogCore/Sources/Versioning.swift +++ b/DatadogCore/Sources/Versioning.swift @@ -1,3 +1,3 @@ // GENERATED FILE: Do not edit directly -internal let __sdkVersion = "2.11.0" +internal let __sdkVersion = "2.12.0" diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift index 4ee4310749..789f042cce 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -139,17 +139,24 @@ class FilesOrchestrator_MetricsTests: XCTestCase { func testWhenNewBatchIsStarted_itSendsBatchClosedMetric() throws { // Given // - request batch to be created - // - request few writes on that batch + // - request few writes on that batch, each after certain delay let orchestrator = createOrchestrator() let expectedWrites: [UInt64] = [10, 5, 2] - try expectedWrites.forEach { writeSize in - _ = try orchestrator.getWritableFile(writeSize: writeSize) - } + let expectedWriteDelays: [TimeInterval] = [ + storage.maxFileAgeForWrite * 0.25, + storage.maxFileAgeForWrite * 0.45, + ] + + _ = try orchestrator.getWritableFile(writeSize: expectedWrites[0]) + dateProvider.advance(bySeconds: expectedWriteDelays[0]) + _ = try orchestrator.getWritableFile(writeSize: expectedWrites[1]) + dateProvider.advance(bySeconds: expectedWriteDelays[1]) + _ = try orchestrator.getWritableFile(writeSize: expectedWrites[2]) // When // - wait more than allowed batch age for writes, so next batch request will create another batch // - then request another batch, which will close the previous one - dateProvider.advance(bySeconds: (storage.maxFileAgeForWrite + 1)) + dateProvider.advance(bySeconds: storage.maxFileAgeForWrite + 1) _ = try orchestrator.getWritableFile(writeSize: 1) // Then @@ -161,7 +168,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "uploader_window": storage.uploaderWindow.toMilliseconds, "batch_size": expectedWrites.reduce(0, +), "batch_events_count": expectedWrites.count, - "batch_duration": (storage.maxFileAgeForWrite + 1).toMilliseconds + "batch_duration": expectedWriteDelays.reduce(0, +).toMilliseconds ]) } } diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift index cb6f09d807..35ce6dc27d 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift @@ -55,17 +55,20 @@ class FilesOrchestratorTests: XCTestCase { } func testWhenSameWritableFileWasUsedMaxNumberOfTimes_itCreatesNewFile() throws { - let orchestrator = configureOrchestrator(using: RelativeDateProvider(advancingBySeconds: 0.001)) + let dateProvider = DateProviderMock() + let orchestrator = configureOrchestrator(using: dateProvider) var previousFile: WritableFile = try orchestrator.getWritableFile(writeSize: 1) // first use of a new file var nextFile: WritableFile for _ in (0..<5) { for _ in (0 ..< performance.maxObjectsInFile).dropLast() { // skip first use + dateProvider.now.addTimeInterval(0.001) nextFile = try orchestrator.getWritableFile(writeSize: 1) XCTAssertEqual(nextFile.name, previousFile.name, "It should reuse the file \(performance.maxObjectsInFile) times") previousFile = nextFile } + dateProvider.now.addTimeInterval(0.001) nextFile = try orchestrator.getWritableFile(writeSize: 1) // first use of a new file XCTAssertNotEqual(nextFile.name, previousFile.name, "It should create a new file when previous one is used \(performance.maxObjectsInFile) times") previousFile = nextFile diff --git a/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift index ea88b9a716..9f45e3b5b7 100644 --- a/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift +++ b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift @@ -12,6 +12,7 @@ import CoreTelephony import DatadogInternal import TestUtilities +@testable import DatadogLogs @testable import DatadogRUM @testable import DatadogCrashReporting @testable import DatadogCore @@ -19,122 +20,266 @@ import TestUtilities /// This suite tests if `CrashContextProvider` gets updated by different SDK components, each updating /// separate part of the `CrashContext` information. class CrashContextProviderTests: XCTestCase { - // MARK: - `DatadogContext` Integration + private let provider = CrashContextCoreProvider() - func testWhenTrackingConsentValueChangesInConsentProvider_thenCrashContextProviderNotifiesNewContext() { - let expectation = self.expectation(description: "Notify new crash context") + // MARK: - Receiving SDK Context + + func testWhenInitialSDKContextIsReceived_itNotifiesCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + } + + func testWhenNextSDKContextIsReceived_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } // Given - let crashContextProvider = CrashContextCoreProvider() - let core = PassthroughCoreMock(messageReceiver: crashContextProvider) - let context: DatadogContext = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() // When - crashContextProvider.onCrashContextChange = { - XCTAssertEqual($0.serverTimeOffset, context.serverTimeOffset) - XCTAssertEqual($0.service, context.service) - XCTAssertEqual($0.env, context.env) - XCTAssertEqual($0.version, context.version) - XCTAssertEqual($0.buildNumber, context.buildNumber) - XCTAssertEqual($0.device.osVersion, context.device.osVersion) - XCTAssertEqual($0.sdkVersion, context.sdkVersion) - XCTAssertEqual($0.source, context.source) - XCTAssertEqual($0.trackingConsent, context.trackingConsent) - DDAssertReflectionEqual($0.userInfo, context.userInfo) - XCTAssertEqual($0.networkConnectionInfo, context.networkConnectionInfo) - XCTAssertEqual($0.carrierInfo, context.carrierInfo) - XCTAssertEqual($0.lastIsAppInForeground, context.applicationStateHistory.currentSnapshot.state.isRunningInForeground) - expectation.fulfill() - } - - core.send(message: .context(context)) + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) // receive next // Then - crashContextProvider.flush() - waitForExpectations(timeout: 0.5, handler: nil) + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) } - // MARK: - `RUMViewEvent` Integration + // MARK: - Receiving RUM View - func testWhenNewRUMView_thenItNotifiesNewCrashContext() throws { - let expectation = self.expectation(description: "Notify new crash context") + func testWhenRUMViewIsReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } // Given - let crashContextProvider = CrashContextCoreProvider() - let core = PassthroughCoreMock(messageReceiver: crashContextProvider) + let sdkContext: DatadogContext = .mockRandom() + let rumView: RUMViewEvent = .mockRandom() - let viewEvent: RUMViewEvent = .mockRandom() + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastRUMViewEvent, rumView, "Last RUM view must be available") + } + + func testWhenSDKContextIsReceivedAfterRUMView_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumView: RUMViewEvent = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() // When - crashContextProvider.onCrashContextChange = { - DDAssertJSONEqual($0.lastRUMViewEvent, viewEvent) - expectation.fulfill() - } + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) - core.send(message: .baggage(key: RUMBaggageKeys.viewEvent, value: viewEvent)) + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastRUMViewEvent, rumView, "Last RUM view must be available even after next SDK context update") + } + + // MARK: - Receiving RUM View Reset + + func testWhenRUMViewResetIsReceivedAfterRUMView_thenItNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let rumView: RUMViewEvent = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewReset, value: true), from: NOPDatadogCore())) // Then - crashContextProvider.flush() - waitForExpectations(timeout: 0.5, handler: nil) + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + XCTAssertNil(crashContext.lastRUMViewEvent, "Last RUM view must reset") } - func testWhenRUMViewReset_thenItNotifiesNewCrashContext() throws { - let expectation = self.expectation(description: "Notify new crash context") - expectation.expectedFulfillmentCount = 2 + func testWhenSDKContextIsReceivedAfterRUMViewReset_thenItNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } // Given - let crashContextProvider = CrashContextCoreProvider() - let core = PassthroughCoreMock(messageReceiver: crashContextProvider) + let rumView: RUMViewEvent = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() - var viewEvent: AnyCodable? = nil + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewReset, value: true), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + XCTAssertNil(crashContext.lastRUMViewEvent, "Last RUM view must reset even after next SDK context update") + } + + // MARK: - Receiving RUM Session State + + func testWhenRUMSessionStateIsReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let rumSessionState: RUMSessionState = .mockRandom() // When - crashContextProvider.onCrashContextChange = { - viewEvent = $0.lastRUMViewEvent - expectation.fulfill() - } + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.sessionState, value: rumSessionState), from: NOPDatadogCore())) - core.send(message: .baggage(key: RUMBaggageKeys.viewEvent, value: RUMViewEvent.mockRandom())) - crashContextProvider.flush() - XCTAssertNotNil(viewEvent) + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastRUMSessionState, rumSessionState, "Last RUM session state must be available") + } + + func testWhenSDKContextIsReceivedAfterRUMSessionState_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumSessionState: RUMSessionState = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() - core.send(message: .baggage(key: RUMBaggageKeys.viewReset, value: true)) + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.sessionState, value: rumSessionState), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) // Then - crashContextProvider.flush() - waitForExpectations(timeout: 0.5, handler: nil) - XCTAssertNil(viewEvent) + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastRUMSessionState, rumSessionState, "Last RUM session state must be available even after next SDK context update") } - // MARK: - RUM Session State Integration + // MARK: - Receiving Global RUM Attributes - func testWhenNewRUMSessionStateIsSentThroughMessageBus_thenItNotifiesNewCrashContext() throws { - let expectation = self.expectation(description: "Notify new crash context") + func testWhenRUMAttributesAreReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } // Given - let crashContextProvider = CrashContextCoreProvider() - let core = PassthroughCoreMock(messageReceiver: crashContextProvider) + let sdkContext: DatadogContext = .mockRandom() + let rumAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) - let sessionState: RUMSessionState = .mockRandom() + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.attributes, value: rumAttributes), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastRUMAttributes, rumAttributes, "Last RUM attributes must be available") + } + + func testWhenSDKContextIsReceivedAfterRUMAttributes_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) + let nextSDKContext: DatadogContext = .mockRandom() // When - crashContextProvider.onCrashContextChange = { - DDAssertJSONEqual($0.lastRUMSessionState, sessionState) - expectation.fulfill() - } + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.attributes, value: rumAttributes), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastRUMAttributes, rumAttributes, "Last RUM attributes must be available even after next SDK context update") + } - core.send(message: .baggage(key: RUMBaggageKeys.sessionState, value: sessionState)) + // MARK: - Receiving Global Log Attributes + + func testWhenLogAttributesAreReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let logAttributes = AnyCodable(mockRandomAttributes()) + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: GlobalLogAttributes.key, value: logAttributes), from: NOPDatadogCore())) // Then - crashContextProvider.flush() - waitForExpectations(timeout: 0.5, handler: nil) + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastLogAttributes, logAttributes, "Last Log attributes must be available") + } + + func testWhenSDKContextIsReceivedAfterLogAttributes_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let logAttributes = AnyCodable(mockRandomAttributes()) + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: GlobalLogAttributes.key, value: logAttributes), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastLogAttributes, logAttributes, "Last Log attributes must be available even after next SDK context update") } // MARK: - Thread safety func testWhenContextIsWrittenAndReadFromDifferentThreads_itRunsAllOperationsSafely() { let provider = CrashContextCoreProvider() - let core = PassthroughCoreMock(messageReceiver: provider) let viewEvent: RUMViewEvent = .mockRandom() let sessionState: RUMSessionState = .mockRandom() @@ -142,17 +287,34 @@ class CrashContextProviderTests: XCTestCase { callConcurrently( closures: [ { _ = provider.currentCrashContext }, - { core.send(message: .context(.mockRandom())) }, - { core.send(message: .baggage(key: RUMBaggageKeys.viewReset, value: true)) }, - { core.send(message: .baggage(key: RUMBaggageKeys.viewEvent, value: viewEvent)) }, - { core.send(message: .baggage(key: RUMBaggageKeys.sessionState, value: sessionState)) }, + { _ = provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore()) }, + { _ = provider.receive(message: .baggage(key: RUMBaggageKeys.viewReset, value: true), from: NOPDatadogCore()) }, + { _ = provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: viewEvent), from: NOPDatadogCore()) }, + { _ = provider.receive(message: .baggage(key: RUMBaggageKeys.sessionState, value: sessionState), from: NOPDatadogCore()) }, ], iterations: 50 ) + // swiftlint:enable opening_brace - // provider retains the core in its queue: - // flush to release the core. provider.flush() - // swiftlint:enable opening_brace + } + + // MARK: - Helpers + + private func DDAssert(crashContext: CrashContext, includes sdkContext: DatadogContext, file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual(crashContext.appLaunchDate, sdkContext.launchTime?.launchDate, file: file, line: line) + XCTAssertEqual(crashContext.serverTimeOffset, sdkContext.serverTimeOffset, file: file, line: line) + XCTAssertEqual(crashContext.service, sdkContext.service, file: file, line: line) + XCTAssertEqual(crashContext.env, sdkContext.env, file: file, line: line) + XCTAssertEqual(crashContext.version, sdkContext.version, file: file, line: line) + XCTAssertEqual(crashContext.buildNumber, sdkContext.buildNumber, file: file, line: line) + XCTAssertEqual(crashContext.device, sdkContext.device, file: file, line: line) + XCTAssertEqual(crashContext.sdkVersion, sdkContext.sdkVersion, file: file, line: line) + XCTAssertEqual(crashContext.source, sdkContext.source, file: file, line: line) + XCTAssertEqual(crashContext.trackingConsent, sdkContext.trackingConsent, file: file, line: line) + DDAssertReflectionEqual(crashContext.userInfo, sdkContext.userInfo, file: file, line: line) + XCTAssertEqual(crashContext.networkConnectionInfo, sdkContext.networkConnectionInfo, file: file, line: line) + XCTAssertEqual(crashContext.carrierInfo, sdkContext.carrierInfo, file: file, line: line) + XCTAssertEqual(crashContext.lastIsAppInForeground, sdkContext.applicationStateHistory.currentSnapshot.state.isRunningInForeground, file: file, line: line) } } diff --git a/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift index 03d157fa49..40e8fd2443 100644 --- a/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift +++ b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift @@ -12,8 +12,10 @@ import TestUtilities @testable import DatadogCrashReporting class CrashContextTests: XCTestCase { - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() + /// This must be the exact encoder used to encode `CrashContext` in production code. + private let encoder = CrashReportingFeature.crashContextEncoder + /// This must be the exact decoder used to decode `CrashContext` in production code. + private let decoder = CrashReportingFeature.crashContextDecoder func testGivenContextWithTrackingConsentSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { let randomConsent: TrackingConsent = .mockRandom() @@ -64,6 +66,40 @@ class CrashContextTests: XCTestCase { ) } + func testGivenContextWithLastRUMAttributesSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomRUMAttributes = Bool.random() ? GlobalRUMAttributes(attributes: mockRandomAttributes()) : nil + + // Given + let context: CrashContext = .mockWith(lastRUMAttributes: randomRUMAttributes) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + DDAssertJSONEqual( + deserializedContext.lastRUMAttributes, + randomRUMAttributes + ) + } + + func testGivenContextWithLastLogttributesSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomLogAttributes = Bool.random() ? AnyCodable(mockRandomAttributes()) : nil + + // Given + let context: CrashContext = .mockWith(lastLogAttributes: randomLogAttributes) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + DDAssertJSONEqual( + deserializedContext.lastLogAttributes, + randomLogAttributes + ) + } + func testGivenContextWithUserInfoSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { let randomUserInfo: UserInfo = .mockRandom() @@ -126,4 +162,22 @@ class CrashContextTests: XCTestCase { let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) XCTAssertEqual(deserializedContext.lastIsAppInForeground, randomIsAppInForeground) } + + func testGivenContextWithAppLaunchDate_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomDate: Date = .mockRandom() + + // Given + let context: CrashContext = .mockWith(appLaunchDate: randomDate) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual( + deserializedContext.appLaunchDate!.timeIntervalSince1970, + randomDate.timeIntervalSince1970, + accuracy: 0.001 // assert with ms precision as we encode dates as ISO8601 string which is lossfull + ) + } } diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift index ea194d4c4f..d1eb48fb65 100644 --- a/DatadogCore/Tests/Datadog/DatadogTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -330,6 +330,7 @@ class DatadogTests: XCTestCase { // mock data is written only after this operation completes - otherwise, migration may delete mocked files. core.readWriteQueue.sync {} + // Given let featureDirectories: [FeatureDirectories] = [ try core.directory.getFeatureDirectories(forFeatureNamed: "logging"), try core.directory.getFeatureDirectories(forFeatureNamed: "tracing"), @@ -338,10 +339,6 @@ class DatadogTests: XCTestCase { let allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] } try allDirectories.forEach { directory in _ = try directory.createFile(named: .mockRandom()) } - // Given - let numberOfFiles = try allDirectories.reduce(0, { acc, nextDirectory in return try acc + nextDirectory.files().count }) - XCTAssertEqual(numberOfFiles, 4, "Each feature stores 2 files - one authorised and one unauthorised") - // When Datadog.clearAllData() diff --git a/DatadogCore/Tests/Datadog/LoggerTests.swift b/DatadogCore/Tests/Datadog/LoggerTests.swift index be2d121063..2e0caaf7eb 100644 --- a/DatadogCore/Tests/Datadog/LoggerTests.swift +++ b/DatadogCore/Tests/Datadog/LoggerTests.swift @@ -7,6 +7,7 @@ import XCTest import TestUtilities import DatadogInternal +import OpenTelemetryApi @testable import DatadogLogs @testable import DatadogTrace @@ -875,6 +876,45 @@ class LoggerTests: XCTestCase { logMatchers[1].assertNoValue(forKey: "dd.span_id") } + func testGivenBundlingWithTraceEnabledAndOpenTelemetryTracerRegistered_whenSendingLog_itContainsActiveSpanAttributes() throws { + core.context = .mockAny() + + Logs.enable(in: core) + Trace.enable(in: core) + + // given + let logger = Logger.create(in: core) + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider(in: core) + ) + + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + + // when + let span = tracer.spanBuilder(spanName: "span") + .setActive(true) + .startSpan() + logger.info("info message 1") + span.end() + logger.info("info message 2") + + // then + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertValue( + forKeyPath: "dd.trace_id", + equals: span.context.traceId.toDatadog().toString(representation: .hexadecimal) + ) + logMatchers[0].assertValue( + forKeyPath: "dd.span_id", + equals: span.context.spanId.toDatadog().toString(representation: .decimal) + ) + logMatchers[1].assertNoValue(forKey: "dd.trace_id") + logMatchers[1].assertNoValue(forKey: "dd.span_id") + } + // MARK: - Log Dates Correction func testGivenTimeDifferenceBetweenDeviceAndServer_whenCollectingLogs_thenLogDateUsesServerTime() throws { diff --git a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 84cd7fab9a..97566f0494 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -144,6 +144,8 @@ extension CrashContext { lastRUMViewEvent: AnyCodable? = nil, lastRUMSessionState: AnyCodable? = nil, lastIsAppInForeground: Bool = .mockAny(), + appLaunchDate: Date? = .mockRandomInThePast(), + lastRUMAttributes: GlobalRUMAttributes? = nil, lastLogAttributes: AnyCodable? = nil ) -> Self { .init( @@ -159,9 +161,11 @@ extension CrashContext { userInfo: userInfo, networkConnectionInfo: networkConnectionInfo, carrierInfo: carrierInfo, + lastIsAppInForeground: lastIsAppInForeground, + appLaunchDate: appLaunchDate, lastRUMViewEvent: lastRUMViewEvent, lastRUMSessionState: lastRUMSessionState, - lastIsAppInForeground: lastIsAppInForeground, + lastRUMAttributes: lastRUMAttributes, lastLogAttributes: lastLogAttributes ) } @@ -180,9 +184,11 @@ extension CrashContext { userInfo: .mockRandom(), networkConnectionInfo: .mockRandom(), carrierInfo: .mockRandom(), + lastIsAppInForeground: .mockRandom(), + appLaunchDate: .mockRandomInThePast(), lastRUMViewEvent: AnyCodable(mockRandomAttributes()), lastRUMSessionState: AnyCodable(mockRandomAttributes()), - lastIsAppInForeground: .mockRandom(), + lastRUMAttributes: GlobalRUMAttributes(attributes: mockRandomAttributes()), lastLogAttributes: AnyCodable(mockRandomAttributes()) ) } diff --git a/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift b/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift index 080a7e8bd6..0ef6a290df 100644 --- a/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift @@ -26,13 +26,15 @@ extension LogsFeature { logEventMapper: LogEventMapper? = nil, requestBuilder: FeatureRequestBuilder = RequestBuilder(), messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), - dateProvider: DateProvider = SystemDateProvider() + dateProvider: DateProvider = SystemDateProvider(), + backtraceReporter: BacktraceReporting? = nil ) -> Self { return .init( logEventMapper: logEventMapper, requestBuilder: requestBuilder, messageReceiver: messageReceiver, - dateProvider: dateProvider + dateProvider: dateProvider, + backtraceReporter: backtraceReporter ) } } diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index 72d2a4c96d..cbc9b059bc 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -246,9 +246,11 @@ extension RUMResourceEvent: RandomMockable { os: .mockRandom(), resource: .init( connect: .init(duration: .mockRandom(), start: .mockRandom()), + decodedBodySize: nil, dns: .init(duration: .mockRandom(), start: .mockRandom()), download: .init(duration: .mockRandom(), start: .mockRandom()), duration: .mockRandom(), + encodedBodySize: nil, firstByte: .init(duration: .mockRandom(), start: .mockRandom()), id: .mockRandom(), method: .mockRandom(), @@ -258,9 +260,11 @@ extension RUMResourceEvent: RandomMockable { type: Bool.random() ? .firstParty : nil ), redirect: .init(duration: .mockRandom(), start: .mockRandom()), + renderBlockingStatus: nil, size: .mockRandom(), ssl: .init(duration: .mockRandom(), start: .mockRandom()), statusCode: .mockRandom(), + transferSize: nil, type: [.native, .image].randomElement()!, url: .mockRandom() ), @@ -387,6 +391,7 @@ extension RUMErrorEvent: RandomMockable { error: .init( binaryImages: nil, category: nil, + csp: nil, handling: nil, handlingStack: nil, id: .mockRandom(), @@ -407,6 +412,7 @@ extension RUMErrorEvent: RandomMockable { sourceType: .mockRandom(), stack: .mockRandom(), threads: nil, + timeSinceAppStart: nil, type: .mockRandom(), wasTruncated: .mockRandom() ), @@ -499,6 +505,7 @@ extension TelemetryConfigurationEvent: RandomMockable { batchProcessingLevel: .mockRandom(), batchSize: .mockAny(), batchUploadFrequency: .mockAny(), + compressIntakeRequests: nil, defaultPrivacyLevel: .mockAny(), forwardConsoleLogs: nil, forwardErrorsToLogs: nil, @@ -516,6 +523,7 @@ extension TelemetryConfigurationEvent: RandomMockable { storeContextsAcrossPages: nil, telemetryConfigurationSampleRate: .mockRandom(), telemetrySampleRate: .mockRandom(), + telemetryUsageSampleRate: nil, traceSampleRate: .mockRandom(), trackBackgroundEvents: .mockRandom(), trackCrossPlatformLongTasks: .mockRandom(), @@ -531,6 +539,7 @@ extension TelemetryConfigurationEvent: RandomMockable { trackResources: .mockRandom(), trackSessionAcrossSubdomains: nil, trackViewsManually: nil, + trackingConsent: nil, useAllowedTracingOrigins: .mockRandom(), useAllowedTracingUrls: nil, useBeforeSend: nil, diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index 78bd435fd5..68e8750259 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -699,6 +699,12 @@ func mockNoOpSessionListener() -> RUM.SessionListener { return { _, _ in } } +internal class FatalErrorContextNotifierMock: FatalErrorContextNotifying { + var sessionState: RUMSessionState? + var view: RUMViewEvent? + var globalAttributes: [String: Encodable] = [:] +} + extension RUMScopeDependencies { static func mockAny() -> RUMScopeDependencies { return mockWith() @@ -713,11 +719,13 @@ extension RUMScopeDependencies { firstPartyHosts: FirstPartyHosts = .init([:]), eventBuilder: RUMEventBuilder = RUMEventBuilder(eventsMapper: .mockNoOp()), rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), + backtraceReporter: BacktraceReporting = BacktraceReporterMock(backtrace: nil), ciTest: RUMCITest? = nil, syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache() + viewCache: ViewCache = ViewCache(), + fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock() ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: featureScope, @@ -728,11 +736,13 @@ extension RUMScopeDependencies { firstPartyHosts: firstPartyHosts, eventBuilder: eventBuilder, rumUUIDGenerator: rumUUIDGenerator, + backtraceReporter: backtraceReporter, ciTest: ciTest, syntheticsTest: syntheticsTest, vitalsReaders: vitalsReaders, onSessionStart: onSessionStart, - viewCache: viewCache + viewCache: viewCache, + fatalErrorContext: fatalErrorContext ) } @@ -745,11 +755,13 @@ extension RUMScopeDependencies { firstPartyHosts: FirstPartyHosts? = nil, eventBuilder: RUMEventBuilder? = nil, rumUUIDGenerator: RUMUUIDGenerator? = nil, + backtraceReporter: BacktraceReporting? = nil, ciTest: RUMCITest? = nil, syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: RUM.SessionListener? = nil, - viewCache: ViewCache? = nil + viewCache: ViewCache? = nil, + fatalErrorContext: FatalErrorContextNotifying? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: self.featureScope, @@ -760,11 +772,13 @@ extension RUMScopeDependencies { firstPartyHosts: firstPartyHosts ?? self.firstPartyHosts, eventBuilder: eventBuilder ?? self.eventBuilder, rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator, + backtraceReporter: backtraceReporter ?? self.backtraceReporter, ciTest: ciTest ?? self.ciTest, syntheticsTest: syntheticsTest ?? self.syntheticsTest, vitalsReaders: vitalsReaders ?? self.vitalsReaders, onSessionStart: onSessionStart ?? self.onSessionStart, - viewCache: viewCache ?? self.viewCache + viewCache: viewCache ?? self.viewCache, + fatalErrorContext: fatalErrorContext ?? self.fatalErrorContext ) } } diff --git a/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift b/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift index efdb4c04ca..5d8d0c4223 100644 --- a/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift +++ b/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift @@ -44,12 +44,6 @@ private extension Dictionary where Key == String, Value == Encodable { } class OTSpanTests: XCTestCase { - #if os(iOS) - private let testModuleName = "DatadogCoreTests_iOS" - #elseif os(tvOS) - private let testModuleName = "DatadogCoreTests_tvOS" - #endif - // MARK: - Test Error Conveniences func testWhenSettingErrorFromSwiftError_itLogsErrorFields() throws { @@ -71,7 +65,7 @@ class OTSpanTests: XCTestCase { XCTAssertEqual( try span.logs[0].otStack(), """ - \(testModuleName)/File.swift:42 + \(moduleName())/File.swift:42 swift error description """ ) @@ -123,7 +117,7 @@ class OTSpanTests: XCTestCase { XCTAssertEqual( try span.logs[0].otStack(), """ - \(testModuleName)/File.swift:42 + \(moduleName())/File.swift:42 Error Domain=DDSpan Code=1 "ns error description" UserInfo={NSLocalizedDescription=ns error description} """ ) @@ -147,7 +141,7 @@ class OTSpanTests: XCTestCase { XCTAssertEqual( try span.logs[0].otStack(), """ - \(testModuleName)/File.swift:42 + \(moduleName())/File.swift:42 """ ) } @@ -175,7 +169,7 @@ class OTSpanTests: XCTestCase { XCTAssertEqual( try span.logs[0].otStack(), """ - \(testModuleName)/File.swift:42 + \(moduleName())/File.swift:42 Thread 0 Crashed: 0 app 0x0000000102bc0d8c 0x102bb8000 + 36236 1 UIKitCore 0x00000001b513d9ac 0x1b4739000 + 10504620 diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index 73e174a98c..6de0000be1 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -401,7 +401,6 @@ class CrashReportReceiverTests: XCTestCase { DDAssertReflectionEqual(sendRUMViewEvent.os, lastRUMViewEvent.os) } - // func testGivenCrashDuringRUMSessionWithActiveView_whenSendingRUMErrorEvent_itIsLinkedToPreviousRUMSessionAndIncludesCrashInformation() throws { let lastRUMViewEvent: RUMViewEvent = .mockRandomWith(crashCount: 0) @@ -720,6 +719,41 @@ class CrashReportReceiverTests: XCTestCase { DDAssertJSONEqual(sendRUMErrorEvent.error.wasTruncated, crashReport.wasTruncated) } + func testGivenCrashDuringRUMSessionWithActiveViewAndLastRUMAttributesAvailable_itSendsEventsWithOverridingAttributes() throws { + let lastRUMViewEvent: RUMViewEvent = .mockRandomWith(crashCount: 0) + let lastRUMAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) + + // Given + let crashDate: Date = .mockDecember15th2019At10AMUTC() + let crashReport: DDCrashReport = .mockWith(date: crashDate) + let crashContext: CrashContext = .mockWith( + trackingConsent: .granted, + lastRUMViewEvent: AnyCodable(lastRUMViewEvent), + lastRUMAttributes: lastRUMAttributes + ) + + let receiver: CrashReportReceiver = .mockWith( + featureScope: featureScope, + dateProvider: RelativeDateProvider(using: crashDate), + sessionSampler: Bool.random() ? .mockKeepAll() : .mockRejectAll(), // no matter sampling (as previous session was sampled) + trackBackgroundEvents: .mockRandom() // no matter BET + ) + + // When + XCTAssertTrue( + receiver.receive(message: .baggage( + key: MessageBusSender.MessageKeys.crash, + value: MessageBusSender.Crash(report: crashReport, context: crashContext) + ), from: NOPDatadogCore()) + ) + + // Then + let sentRUMViewAttributes = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self)[0].context?.contextInfo) + let sentRUMErrorAttributes = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0].context?.contextInfo) + DDAssertJSONEqual(sentRUMViewAttributes, lastRUMAttributes.attributes) + DDAssertJSONEqual(sentRUMErrorAttributes, lastRUMAttributes.attributes) + } + // MARK: - Testing Uploaded Data - Crashes During RUM Session With No Active View func testGivenCrashDuringRUMSessionWithNoActiveView_whenSendingRUMViewEvent_itIsLinkedToPreviousRUMSessionAndIncludesErrorInformation() throws { @@ -1017,6 +1051,74 @@ class CrashReportReceiverTests: XCTestCase { ) } + func testGivenCrashDuringRUMSessionWithNoActiveViewAndLastRUMAttributesAvailable_itSendsEventsWithOverridingAttributes() throws { + func test( + lastRUMSessionState: RUMSessionState, + launchInForeground: Bool, + backgroundEventsTrackingEnabled: Bool + ) throws { + let featureScope = FeatureScopeMock() + + // Given + let lastRUMAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) + let crashDate: Date = .mockDecember15th2019At10AMUTC() + let crashReport: DDCrashReport = .mockWith( + date: crashDate, + type: .mockRandom() + ) + let dateCorrectionOffset: TimeInterval = .mockRandom() + let crashContext: CrashContext = .mockWith( + serverTimeOffset: dateCorrectionOffset, + service: .mockRandom(), + version: .mockRandom(), + buildNumber: .mockRandom(), + source: .mockRandom(), + trackingConsent: .granted, + userInfo: .mockRandom(), + networkConnectionInfo: .mockRandom(), + carrierInfo: .mockRandom(), + lastRUMViewEvent: nil, // means there was no active RUM view + lastRUMSessionState: AnyCodable(lastRUMSessionState), // means there was RUM session (sampled) + lastIsAppInForeground: launchInForeground, + lastRUMAttributes: lastRUMAttributes + ) + + let receiver: CrashReportReceiver = .mockWith( + featureScope: featureScope, + applicationID: .mockRandom(among: .alphanumerics), + dateProvider: RelativeDateProvider(using: crashDate), + sessionSampler: Bool.random() ? .mockKeepAll() : .mockRejectAll(), // no matter sampling (as previous session was sampled) + trackBackgroundEvents: backgroundEventsTrackingEnabled + ) + + // When + XCTAssertTrue( + receiver.receive(message: .baggage( + key: MessageBusSender.MessageKeys.crash, + value: MessageBusSender.Crash(report: crashReport, context: crashContext) + ), from: NOPDatadogCore()) + ) + + // Then + let sentRUMViewAttributes = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self)[0].context?.contextInfo) + let sentRUMErrorAttributes = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0].context?.contextInfo) + DDAssertJSONEqual(sentRUMViewAttributes, lastRUMAttributes.attributes) + DDAssertJSONEqual(sentRUMErrorAttributes, lastRUMAttributes.attributes) + } + + try test( + lastRUMSessionState: .mockWith(isInitialSession: true, hasTrackedAnyView: false), // when initial session with no views history + launchInForeground: true, // launch in foreground + backgroundEventsTrackingEnabled: .mockRandom() // no matter BET + ) + + try test( + lastRUMSessionState: .mockRandom(), // any sampled session + launchInForeground: false, // launch in background + backgroundEventsTrackingEnabled: true // BET enabled + ) + } + // MARK: - Testing Uploaded Data - Crashes During App Launch func testGivenCrashDuringAppLaunch_whenSending_itIsSendAsRUMErrorInNewRUMSession() throws { @@ -1316,4 +1418,60 @@ class CrashReportReceiverTests: XCTestCase { expectViewURL: RUMOffViewEventsHandlingRule.Constants.backgroundViewURL ) } + + func testGivenCrashDuringAppLaunchAndLastRUMAttributesAvailable_itSendsEventsWithOverridingAttributes() throws { + func test( + launchInForeground: Bool, + backgroundEventsTrackingEnabled: Bool + ) throws { + let featureScope = FeatureScopeMock() + let lastRUMAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) + + // Given + let crashDate: Date = .mockDecember15th2019At10AMUTC() + let crashReport: DDCrashReport = .mockWith( + date: crashDate, + type: .mockRandom() + ) + + let crashContext: CrashContext = .mockWith( + trackingConsent: .granted, + lastRUMViewEvent: nil, // means there was no RUM session (it crashed during app launch) + lastRUMSessionState: nil, // means there was no RUM session (it crashed during app launch) + lastIsAppInForeground: launchInForeground, + lastRUMAttributes: lastRUMAttributes + ) + + let receiver: CrashReportReceiver = .mockWith( + featureScope: featureScope, + applicationID: .mockRandom(), + dateProvider: RelativeDateProvider(using: crashDate), + trackBackgroundEvents: backgroundEventsTrackingEnabled + ) + + // When + XCTAssertTrue( + receiver.receive(message: .baggage( + key: MessageBusSender.MessageKeys.crash, + value: MessageBusSender.Crash(report: crashReport, context: crashContext) + ), from: NOPDatadogCore()) + ) + + // Then + let sentRUMViewAttributes = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self)[0].context?.contextInfo) + let sentRUMErrorAttributes = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0].context?.contextInfo) + DDAssertJSONEqual(sentRUMViewAttributes, lastRUMAttributes.attributes) + DDAssertJSONEqual(sentRUMErrorAttributes, lastRUMAttributes.attributes) + } + + try test( + launchInForeground: true, // launch in foreground + backgroundEventsTrackingEnabled: .mockRandom() // no matter BET + ) + + try test( + launchInForeground: false, // launch in background + backgroundEventsTrackingEnabled: true // BET enabled + ) + } } diff --git a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift index e10170d75e..98a79ed1c7 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift @@ -1220,7 +1220,7 @@ class RUMMonitorTests: XCTestCase { case 10: monitor.addAction(type: .tap, name: .mockRandom()) case 11: monitor.addAttribute(forKey: String.mockRandom(), value: String.mockRandom()) case 12: monitor.removeAttribute(forKey: String.mockRandom()) - case 13: monitor.dd.debug = .mockRandom() + case 13: monitor.debug = .mockRandom() default: break } } @@ -1435,7 +1435,7 @@ class RUMMonitorTests: XCTestCase { for matcher in matchers.filterTelemetry() { // Application Start/Launch happens too early to have attributes set. if (try? matcher.attribute(forKeyPath: "action.type")) == "application_start" || - (try? matcher.attribute(forKeyPath: "view.name")) == "ApplicationLaunch"{ + (try? matcher.attribute(forKeyPath: "view.name")) == "ApplicationLaunch" { continue } expectedAttributes.forEach { attrKey, attrValue in diff --git a/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift index cbb5d5fc86..8713dbcd0d 100644 --- a/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift @@ -29,7 +29,7 @@ class TelemetryReceiverTests: XCTestCase { callConcurrently( closures: [ { core.telemetry.debug(id: .mockRandom(), message: "telemetry debug") }, - { core.telemetry.error(id: .mockRandom(), message: "telemetry error", kind: nil, stack: nil) }, + { core.telemetry.error(id: .mockRandom(), message: "telemetry error", kind: "error.kind", stack: "error.stack") }, { core.telemetry.configuration(batchSize: .mockRandom()) }, { core.set( diff --git a/DatadogCore/Tests/Datadog/Core/Utils/CoreMetricsTests.swift b/DatadogCore/Tests/Datadog/SDKMetrics/BatchMetricsTests.swift similarity index 97% rename from DatadogCore/Tests/Datadog/Core/Utils/CoreMetricsTests.swift rename to DatadogCore/Tests/Datadog/SDKMetrics/BatchMetricsTests.swift index 1de4a486b3..d02ee85b75 100644 --- a/DatadogCore/Tests/Datadog/Core/Utils/CoreMetricsTests.swift +++ b/DatadogCore/Tests/Datadog/SDKMetrics/BatchMetricsTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import DatadogCore -class CoreMetricsTests: XCTestCase { +class BatchMetricsTests: XCTestCase { func testBatchRemovalReasonFormatting() { typealias RemovalReason = BatchDeletedMetric.RemovalReason diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index c5a1841143..ae7d7fb263 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -732,7 +732,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) tracer.inject(spanContext: injectedContext, writer: writer) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -753,7 +753,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .single) + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .single, traceContextInjection: .all) tracer.inject(spanContext: injectedContext, writer: writer) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -774,7 +774,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .multiple) + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .multiple, traceContextInjection: .all) tracer.inject(spanContext: injectedContext, writer: writer) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -795,7 +795,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = W3CHTTPHeadersWriter(samplingStrategy: .headBased) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) tracer.inject(spanContext: injectedContext, writer: writer) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) diff --git a/DatadogCore/Tests/Datadog/Tracing/OTelSpanTests.swift b/DatadogCore/Tests/Datadog/Tracing/OTelSpanTests.swift new file mode 100644 index 0000000000..6590c9f589 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Tracing/OTelSpanTests.swift @@ -0,0 +1,115 @@ +/* + * 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 +import OpenTelemetryApi + +@testable import DatadogLogs +@testable import DatadogTrace + +final class OTelSpanTests: XCTestCase { + func testAddEvent() { + let core = DatadogCoreProxy() + defer { core.flushAndTearDown() } + + Logs.enable(in: core) + Trace.enable(in: core) + + // Given + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider(in: core) + ) + + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + + let span = tracer + .spanBuilder(spanName: "OperationName") + .startSpan() + + // When + let attributes: [String: OpenTelemetryApi.AttributeValue] = .leafMock() + span.addEvent(name: "Otel Span Event", attributes: attributes, timestamp: Date()) + + // Then + let logs: [LogEvent] = core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self) + XCTAssertEqual(logs.count, 0) + } + + func testContextProviderSetActive_givenParentSpan() throws { + let core = DatadogCoreProxy() + defer { core.flushAndTearDown() } + + Trace.enable(in: core) + + // Given + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider(in: core) + ) + + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + + let parentSpan = tracer + .spanBuilder(spanName: "ParentSpan") + .startSpan() + + // When + OpenTelemetry.instance.contextProvider.setActiveSpan(parentSpan) + + let childSpan = tracer + .spanBuilder(spanName: "ChildSpan") + .startSpan() + + childSpan.end() + parentSpan.end() + + // Then + let spans = try core.waitAndReturnSpanMatchers() + XCTAssertEqual(spans.count, 2) + + let childSpanMatcher = spans[0] + let parentSpanMatcher = spans[1] + + XCTAssertEqual(try parentSpanMatcher.traceID(), try childSpanMatcher.traceID()) + XCTAssertEqual(try parentSpanMatcher.spanID(), try childSpanMatcher.parentSpanID()) + } +} + +extension Dictionary where Key == String, Value == OpenTelemetryApi.AttributeValue { + static func mock() -> Self { + return [ + "string": .string("value"), + "bool": .bool(true), + "int": .int(2), + "double": .double(2.0), + "stringArray": .stringArray(["value1", "value2"]), + "boolArray": .boolArray([true, false]), + "intArray": .intArray([1, 2]), + "doubleArray": .doubleArray([1.0, 2.0]), + "set": .set(.init(labels: .leafMock())) + ] + } + + static func leafMock() -> Self { + return [ + "string": .string("value"), + "bool": .bool(true), + "int": .int(2), + "double": .double(2.0), + "stringArray": .stringArray(["value1", "value2"]), + "boolArray": .boolArray([true, false]), + "intArray": .intArray([1, 2]), + "doubleArray": .doubleArray([1.0, 2.0]) + ] + } +} diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift index c16950c36d..8b88bf6b4f 100644 --- a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -39,7 +39,8 @@ class TracingURLSessionHandlerTests: XCTestCase { distributedTraceSampler: .mockKeepAll(), firstPartyHosts: .init([ "www.example.com": [.datadog] - ]) + ]), + traceContextInjection: .all ) } diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift index 07e090358f..e2806786cf 100644 --- a/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift @@ -160,6 +160,50 @@ class DDRUMMonitorTests: XCTestCase { XCTAssertTrue(DDRUMMonitor.shared().swiftRUMMonitor is Monitor) } + func testProvidingCurrentSessionID() throws { + let callSessionIDCallback = expectation(description: "call session ID callback") + var currentSessionID: String? = nil + + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + objcRUMMonitor.currentSessionID { sessionID in + currentSessionID = sessionID + callSessionIDCallback.fulfill() + } + + waitForExpectations(timeout: 0.5) + let sessionID = try XCTUnwrap(currentSessionID) + XCTAssertTrue(sessionID.matches(regex: .uuidRegex)) + } + + func testStoppingSession() throws { + let callSessionIDCallback = expectation(description: "call session ID callback twice") + callSessionIDCallback.expectedFulfillmentCount = 2 + var sessionID1: String? = nil + var sessionID2: String? = nil + + // Given + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + objcRUMMonitor.currentSessionID { sessionID in + sessionID1 = sessionID + callSessionIDCallback.fulfill() + } + + // When + objcRUMMonitor.stopSession() + objcRUMMonitor.startView(key: "key", name: "AnyView", attributes: [:]) + + // Then + objcRUMMonitor.currentSessionID { sessionID in + sessionID2 = sessionID + callSessionIDCallback.fulfill() + } + + waitForExpectations(timeout: 0.5) + XCTAssertNotEqual(try XCTUnwrap(sessionID1), try XCTUnwrap(sessionID2)) + } + func testSendingViewEvents() throws { RUM.enable(with: config) @@ -391,4 +435,28 @@ class DDRUMMonitorTests: XCTestCase { XCTAssertNil(try? viewEvents[0].attribute(forKeyPath: "context.global-attribute2") as String) XCTAssertEqual(try viewEvents[0].attribute(forKeyPath: "context.event-attribute1"), "foo1") } + + func testEvaluatingFeatureFlags() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.addFeatureFlagEvaluation(name: "flag1", value: "value1") + objcRUMMonitor.addFeatureFlagEvaluation(name: "flag2", value: true) + + let viewEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self) + let lastView = try XCTUnwrap(viewEvents.last) + XCTAssertEqual(lastView.featureFlags!.featureFlagsInfo["flag1"] as? AnyEncodable, AnyEncodable("value1")) + XCTAssertEqual(lastView.featureFlags!.featureFlagsInfo["flag2"] as? AnyEncodable, AnyEncodable(true)) + } + + func testChangingDebugFlag() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.debug = true + XCTAssertTrue(objcRUMMonitor.swiftRUMMonitor.debug) + + objcRUMMonitor.debug = false + XCTAssertFalse(objcRUMMonitor.swiftRUMMonitor.debug) + } } diff --git a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift index 22c6a1cb70..7119f7d66b 100644 --- a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift @@ -203,7 +203,10 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let objcWriter = DDHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -222,13 +225,13 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + let objcWriter = DDHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + traceContextInjection: .sampled + ) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) - let expectedHTTPHeaders = [ - "x-datadog-sampling-priority": "0", - ] - XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) + XCTAssertEqual(objcWriter.traceHeaderFields, [:]) } func testInjectingSpanContextToInvalidCarrierOrFormat() throws { @@ -236,7 +239,10 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let objcValidWriter = DDHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) @@ -256,7 +262,10 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let objcWriter = DDB3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -272,7 +281,10 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + let objcWriter = DDB3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + traceContextInjection: .all + ) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -286,7 +298,10 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let objcValidWriter = DDB3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) @@ -306,7 +321,10 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let objcWriter = DDW3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -323,7 +341,10 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + let objcWriter = DDW3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + traceContextInjection: .all + ) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -338,7 +359,10 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let objcValidWriter = DDW3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) 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 e315e3ec02..35094c851d 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m @@ -20,9 +20,11 @@ @implementation DDB3HTTPHeadersWriter_apiTests - (void)testInitWithSamplingRate { [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] - injectEncoding:DDInjectEncodingSingle]; + injectEncoding:DDInjectEncodingSingle + traceContextInjection:DDTraceContextInjectionAll]; [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] - injectEncoding:DDInjectEncodingMultiple]; + injectEncoding:DDInjectEncodingMultiple + traceContextInjection:DDTraceContextInjectionAll]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m index 7c0f0e9ffc..1b03444744 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m @@ -19,8 +19,8 @@ @implementation DDHTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased]]; - [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50]]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] traceContextInjection:DDTraceContextInjectionAll]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] traceContextInjection:DDTraceContextInjectionAll]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m index ab6b08ea25..4b144cc68c 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m @@ -134,5 +134,4 @@ - (void)testDDRUMConfigurationAPI { #pragma clang diagnostic pop - @end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m index 8c6eaa7586..d603998a99 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m @@ -52,6 +52,9 @@ - (void)testDDRUMMonitorAPI { UIViewController *anyVC = [UIViewController new]; DDRUMMonitor *monitor = [DDRUMMonitor shared]; + [monitor currentSessionIDWithCompletion:^(NSString * _Nullable sessionID) {}]; + [monitor stopSession]; + [monitor startViewWithViewController:anyVC name:@"" attributes:@{}]; [monitor stopViewWithViewController:anyVC attributes:@{}]; [monitor startViewWithKey:@"" name:nil attributes:@{}]; @@ -72,6 +75,10 @@ - (void)testDDRUMMonitorAPI { [monitor stopActionWithType:DDRUMActionTypeSwipe name:nil attributes:@{}]; [monitor addActionWithType:DDRUMActionTypeTap name:@"" attributes:@{}]; [monitor addAttributeForKey:@"" value:@""]; + [monitor addFeatureFlagEvaluationWithName: @"name" value: @"value"]; + + [monitor setDebug:YES]; + [monitor setDebug:NO]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m index 1c5486d0ae..ddaaa620f2 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m @@ -19,8 +19,8 @@ @implementation DDW3CHTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased]]; - [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50]]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] traceContextInjection:DDTraceContextInjectionAll]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] traceContextInjection:DDTraceContextInjectionAll]; } diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDWebViewTracking+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDWebViewTracking+apiTests.m new file mode 100644 index 0000000000..f5082efaaa --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDWebViewTracking+apiTests.m @@ -0,0 +1,50 @@ +/* +* 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 +@import DatadogWebViewTracking; +@import WebKit; + +@interface WebViewMock: WKWebView +@end + +@implementation WebViewMock +@end + +// MARK: - DDWebViewTracking tests + +@interface DDWebViewTracking_apiTests : XCTestCase +@end + +/* + * `WebViewTracking` APIs smoke tests - minimal assertions, mainly check if the interface is available to Objc. + */ +@implementation DDWebViewTracking_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testDDWebViewTrackingAPI { + WebViewMock *webView = [WebViewMock new]; + [DDWebViewTracking enableWithWebView:webView + hosts:[NSSet setWithArray:@[@"host1.com", @"host2.com"]] + logsSampleRate:100.0 + with:nil]; + [DDWebViewTracking disableWithWebView:webView]; +} + +- (void)testDDWebViewTrackingSessionReplayConfigurationAPI { + DDWebViewTrackingSessionReplayConfiguration *config = [[DDWebViewTrackingSessionReplayConfiguration alloc] init]; + XCTAssertEqual(config.privacyLevel, DDPrivacyLevelMask); + config.privacyLevel = DDPrivacyLevelAllow; + XCTAssertEqual(config.privacyLevel, DDPrivacyLevelAllow); + config.privacyLevel = DDPrivacyLevelMaskUserInput; + XCTAssertEqual(config.privacyLevel, DDPrivacyLevelMaskUserInput); +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCrashReporting.podspec b/DatadogCrashReporting.podspec index 7193a3ee0b..80d2677cce 100644 --- a/DatadogCrashReporting.podspec +++ b/DatadogCrashReporting.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "DatadogCrashReporting" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.source_files = "DatadogCrashReporting/Sources/**/*.swift" s.dependency 'DatadogInternal', s.version.to_s - s.dependency 'PLCrashReporter', '~> 1.11.1' + s.dependency 'PLCrashReporter', '~> 1.11.2' s.resource_bundle = { "DatadogCrashReporting" => "DatadogCrashReporting/Resources/PrivacyInfo.xcprivacy" diff --git a/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift b/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift index 09b87afc7e..c55b307da6 100644 --- a/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift +++ b/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift @@ -13,6 +13,9 @@ import DatadogInternal /// Note: as it gets saved along with the crash report during process interruption, it's good /// to keep this data well-packed and as small as possible. internal struct CrashContext: Codable, Equatable { + /// The Application Launch Date + var appLaunchDate: Date? + /// Interval between device and server time. /// /// The value can change as the device continue to sync with the server. @@ -60,18 +63,21 @@ internal struct CrashContext: Codable, Equatable { /// not support telephony services. let carrierInfo: CarrierInfo? + /// The last _"Is app in foreground?"_ information from crashed app process. + let lastIsAppInForeground: Bool + /// The last RUM view in crashed app process. var lastRUMViewEvent: AnyCodable? /// State of the last RUM session in crashed app process. var lastRUMSessionState: AnyCodable? - /// The last _"Is app in foreground?"_ information from crashed app process. - let lastIsAppInForeground: Bool - /// Last global log attributes, set with Logs.addAttribute / Logs.removeAttribute var lastLogAttributes: AnyCodable? + /// Last global RUM attributes. It gets updated with adding or removing attributes on `RUMMonitor`. + var lastRUMAttributes: GlobalRUMAttributes? + // MARK: - Initialization init( @@ -87,9 +93,11 @@ internal struct CrashContext: Codable, Equatable { userInfo: UserInfo?, networkConnectionInfo: NetworkConnectionInfo?, carrierInfo: CarrierInfo?, + lastIsAppInForeground: Bool, + appLaunchDate: Date?, lastRUMViewEvent: AnyCodable?, lastRUMSessionState: AnyCodable?, - lastIsAppInForeground: Bool, + lastRUMAttributes: GlobalRUMAttributes?, lastLogAttributes: AnyCodable? ) { self.serverTimeOffset = serverTimeOffset @@ -104,9 +112,11 @@ internal struct CrashContext: Codable, Equatable { self.userInfo = userInfo self.networkConnectionInfo = networkConnectionInfo self.carrierInfo = carrierInfo + self.lastIsAppInForeground = lastIsAppInForeground + self.appLaunchDate = appLaunchDate self.lastRUMViewEvent = lastRUMViewEvent self.lastRUMSessionState = lastRUMSessionState - self.lastIsAppInForeground = lastIsAppInForeground + self.lastRUMAttributes = lastRUMAttributes self.lastLogAttributes = lastLogAttributes } @@ -114,6 +124,7 @@ internal struct CrashContext: Codable, Equatable { _ context: DatadogContext, lastRUMViewEvent: AnyCodable?, lastRUMSessionState: AnyCodable?, + lastRUMAttributes: GlobalRUMAttributes?, lastLogAttributes: AnyCodable? ) { self.serverTimeOffset = context.serverTimeOffset @@ -132,7 +143,10 @@ internal struct CrashContext: Codable, Equatable { self.lastRUMViewEvent = lastRUMViewEvent self.lastRUMSessionState = lastRUMSessionState + self.lastRUMAttributes = lastRUMAttributes self.lastLogAttributes = lastLogAttributes + + self.appLaunchDate = context.launchTime?.launchDate } static func == (lhs: CrashContext, rhs: CrashContext) -> Bool { @@ -148,6 +162,7 @@ internal struct CrashContext: Codable, Equatable { lhs.lastIsAppInForeground == rhs.lastIsAppInForeground && lhs.userInfo?.id == rhs.userInfo?.id && lhs.userInfo?.name == rhs.userInfo?.name && - lhs.userInfo?.email == rhs.userInfo?.email + lhs.userInfo?.email == rhs.userInfo?.email && + lhs.appLaunchDate == rhs.appLaunchDate } } diff --git a/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift b/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift index 1e360acc4f..6d71594ec5 100644 --- a/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift +++ b/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift @@ -43,6 +43,10 @@ internal class CrashContextCoreProvider: CrashContextProvider { didSet { _context?.lastLogAttributes = logAttributes } } + private var rumAttributes: GlobalRUMAttributes? { + didSet { _context?.lastRUMAttributes = rumAttributes } + } + // MARK: - CrashContextProviderType var currentCrashContext: CrashContext? { @@ -71,6 +75,9 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { /// This key references the global log attributes static let logAttributes = "global-log-attributes" + + /// The key referencing ``DatadogInternal.GlobalRUMAttributes`` value holding RUM global attributes. + static let rumAttributes = "global-rum-attributes" } func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { @@ -85,6 +92,8 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { updateSessionState(with: baggage, to: core) case .baggage(let label, let baggage) where label == RUMBaggageKeys.logAttributes: updateLogAttributes(with: baggage, to: core) + case .baggage(let label, let baggage) where label == RUMBaggageKeys.rumAttributes: + updateRUMAttributes(with: baggage, to: core) default: return false } @@ -96,11 +105,16 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { /// /// - Parameter context: The updated core context. private func update(context: DatadogContext) { - queue.async { + queue.async { [weak self] in + guard let self = self else { + return + } + let crashContext = CrashContext( context, lastRUMViewEvent: self.viewEvent, lastRUMSessionState: self.sessionState, + lastRUMAttributes: self.rumAttributes, lastLogAttributes: self.logAttributes ) @@ -111,9 +125,9 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { } private func updateRUMView(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { - queue.async { [weak core] in + queue.async { [weak core, weak self] in do { - self.viewEvent = try baggage.decode(type: AnyCodable.self) + self?.viewEvent = try baggage.decode(type: AnyCodable.self) } catch { core?.telemetry .error("Fails to decode RUM view event from Crash Reporting", error: error) @@ -122,10 +136,10 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { } private func resetRUMView(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { - queue.async { [weak core] in + queue.async { [weak core, weak self] in do { if try baggage.decode(type: Bool.self) { - self.viewEvent = nil + self?.viewEvent = nil } } catch { core?.telemetry @@ -135,9 +149,9 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { } private func updateSessionState(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { - queue.async { [weak core] in + queue.async { [weak core, weak self] in do { - self.sessionState = try baggage.decode(type: AnyCodable.self) + self?.sessionState = try baggage.decode(type: AnyCodable.self) } catch { core?.telemetry .error("Fails to decode RUM session state from Crash Reporting", error: error) @@ -146,9 +160,20 @@ extension CrashContextCoreProvider: FeatureMessageReceiver { } private func updateLogAttributes(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { - queue.async { [weak core] in + queue.async { [weak core, weak self] in + do { + self?.logAttributes = try baggage.decode(type: AnyCodable.self) + } catch { + core?.telemetry + .error("Fails to decode log attributes from Crash Reporting", error: error) + } + } + } + + private func updateRUMAttributes(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { + queue.async { [weak core, weak self] in do { - self.logAttributes = try baggage.decode(type: AnyCodable.self) + self?.rumAttributes = try baggage.decode(type: GlobalRUMAttributes.self) } catch { core?.telemetry .error("Fails to decode log attributes from Crash Reporting", error: error) diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift index 8d34e42b33..fe99801329 100644 --- a/DatadogCrashReporting/Sources/CrashReportingFeature.swift +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -90,13 +90,25 @@ internal final class CrashReportingFeature: DatadogFeature { /// Note: this `JSONEncoder` must have the same configuration as the `JSONEncoder` used later for writing payloads to uploadable files. /// Otherwise the format of data read and uploaded from crash report context will be different than the format of data retrieved from the user /// and written directly to uploadable file. - private let crashContextEncoder: JSONEncoder = .dd.default() + internal static let crashContextEncoder: JSONEncoder = .dd.default() /// JSON decoder used for reading `CrashContext` from JSON `Data` injected to crash report. - private let crashContextDecoder = JSONDecoder() + /// Note: it must follow a configuration that enables reading data encoded with `crashContextEncoder`. + internal static let crashContextDecoder: JSONDecoder = { + var decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + guard let date = iso8601DateFormatter.date(from: dateString) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: Requires ISO8601.") + } + return date + } + return decoder + }() private func encode(crashContext: CrashContext) -> Data? { do { - return try crashContextEncoder.encode(crashContext) + return try CrashReportingFeature.crashContextEncoder.encode(crashContext) } catch { DD.logger.error( """ @@ -113,7 +125,7 @@ internal final class CrashReportingFeature: DatadogFeature { private func decode(crashContextData: Data) -> CrashContext? { do { - return try crashContextDecoder.decode(CrashContext.self, from: crashContextData) + return try CrashReportingFeature.crashContextDecoder.decode(CrashContext.self, from: crashContextData) } catch { DD.logger.error( """ diff --git a/DatadogInternal.podspec b/DatadogInternal.podspec index 7af446f147..8bf055e353 100644 --- a/DatadogInternal.podspec +++ b/DatadogInternal.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "DatadogInternal" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Datadog Internal Package. This module is not for public use." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '11.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } - + s.source_files = ["DatadogInternal/Sources/**/*.swift"] end diff --git a/DatadogInternal/Sources/Attributes/Attributes.swift b/DatadogInternal/Sources/Attributes/Attributes.swift index 74457c09f2..5ee57a30f0 100644 --- a/DatadogInternal/Sources/Attributes/Attributes.swift +++ b/DatadogInternal/Sources/Attributes/Attributes.swift @@ -153,6 +153,9 @@ public struct CrossPlatformAttributes { /// Override the `source_type` of errors reported by the native crash handler. This is used on /// platforms that can supply extra steps or information on a native crash (such as Unity's IL2CPP) public static let nativeSourceType = "_dd.native_source_type" + + /// Add "binary images" to the reportted error to assist with symbolication. Used by Unity for IL2CPP symbolicaiton + public static let includeBinaryImages = "_dd.error.include_binary_images" } public struct LaunchArguments { diff --git a/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift b/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift index e22ef97166..7d53e3be5d 100644 --- a/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift +++ b/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift @@ -42,3 +42,5 @@ public struct BinaryImage: Codable, PassthroughAnyCodable { case maxAddress = "max_address" } } + +extension BinaryImage: Equatable {} diff --git a/DatadogInternal/Sources/Models/RUM/GlobalRUMAttributes.swift b/DatadogInternal/Sources/Models/RUM/GlobalRUMAttributes.swift new file mode 100644 index 0000000000..a4e03fdcaa --- /dev/null +++ b/DatadogInternal/Sources/Models/RUM/GlobalRUMAttributes.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +public struct GlobalRUMAttributes: Codable, PassthroughAnyCodable { + public let attributes: [AttributeKey: AttributeValue] + + public init(attributes: [AttributeKey: AttributeValue]) { + self.attributes = attributes + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: DynamicCodingKey.self) + try attributes.forEach { + try container.encode(AnyEncodable($1), forKey: DynamicCodingKey($0)) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKey.self) + attributes = try container.allKeys + .reduce(into: [:]) { acc, next in acc[next.stringValue] = try container.decode(AnyCodable.self, forKey: next) } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift index 7f34da3e7a..6f1cd301f9 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift @@ -59,6 +59,9 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { private let samplingStrategy: TraceSamplingStrategy + /// Defines whether the trace context should be injected into all requests or only sampled ones. + private let traceContextInjection: TraceContextInjection + /// The telemetry header encoding used by the writer. private let injectEncoding: InjectEncoding @@ -85,7 +88,8 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { ) { self.init( samplingStrategy: .custom(sampleRate: sampleRate), - injectEncoding: injectEncoding + injectEncoding: injectEncoding, + traceContextInjection: .all ) } @@ -93,12 +97,15 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. + /// - Parameter traceContextInjection: The trace context injection strategy, with `.all` as the default. public init( samplingStrategy: TraceSamplingStrategy, - injectEncoding: InjectEncoding = .single + injectEncoding: InjectEncoding = .single, + traceContextInjection: TraceContextInjection = .all ) { self.samplingStrategy = samplingStrategy self.injectEncoding = injectEncoding + self.traceContextInjection = traceContextInjection } /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. @@ -112,30 +119,35 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { typealias Constants = B3HTTPHeaders.Constants - switch injectEncoding { - case .multiple: - traceHeaderFields = [ - B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue - ] - - 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 sampled { - traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [ - String(traceContext.traceID, representation: .hexadecimal32Chars), - String(traceContext.spanID, representation: .hexadecimal16Chars), - sampled ? Constants.sampledValue : Constants.unsampledValue, - traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } + switch (traceContextInjection, sampled) { + case (.all, _), (.sampled, true): + switch injectEncoding { + case .multiple: + traceHeaderFields = [ + B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue ] - .compactMap { $0 } - .joined(separator: Constants.b3Separator) - } else { - traceHeaderFields[B3HTTPHeaders.Single.b3Field] = Constants.unsampledValue + + 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 sampled { + traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [ + 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) + } else { + traceHeaderFields[B3HTTPHeaders.Single.b3Field] = Constants.unsampledValue + } } + case (.sampled, false): + break } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift index b634f27d54..1d38b2e43f 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift @@ -35,6 +35,7 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { public private(set) var traceHeaderFields: [String: String] = [:] private let samplingStrategy: TraceSamplingStrategy + private let traceContextInjection: TraceContextInjection /// Initializes the headers writer. /// @@ -49,14 +50,19 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// - 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(samplingStrategy: .custom(sampleRate: sampleRate)) + self.init(samplingStrategy: .custom(sampleRate: sampleRate), traceContextInjection: .all) } /// Initializes the headers writer. /// /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. - public init(samplingStrategy: TraceSamplingStrategy) { + /// - Parameter traceContextInjection: The strategy for injecting trace context into requests. + public init( + samplingStrategy: TraceSamplingStrategy, + traceContextInjection: TraceContextInjection + ) { self.samplingStrategy = samplingStrategy + self.traceContextInjection = traceContextInjection } /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. @@ -68,14 +74,16 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { let sampler = samplingStrategy.sampler(for: traceContext) let sampled = sampler.sample() - traceHeaderFields = [ - TracingHTTPHeaders.samplingPriorityField: sampled ? "1" : "0" - ] - - if sampled { + switch (traceContextInjection, sampled) { + case (.all, _), (.sampled, true): + traceHeaderFields = [ + TracingHTTPHeaders.samplingPriorityField: sampled ? "1" : "0" + ] traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo) traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal) traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)" + case (.sampled, false): + break } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift index c1f8f81495..4ddaedbe3f 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift @@ -197,7 +197,7 @@ extension NetworkInstrumentationFeature { interception.register(trace: traceContext) } - if let origin = request.allHTTPHeaderFields?[TracingHTTPHeaders.originField] { + if let origin = request.ddOriginHeaderValue { interception.register(origin: origin) } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift new file mode 100644 index 0000000000..b890b32ceb --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift @@ -0,0 +1,16 @@ +/* + * 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 + +/// Defines whether the trace context should be injected into all requests or only sampled ones. +public enum TraceContextInjection { + /// Injects trace context into all requests irrespective of the sampling decision. + case all + + /// Injects trace context only into sampled requests. + case sampled +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift index d949b3c5a5..0fbe701f6d 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift @@ -15,7 +15,7 @@ public typealias DDURLSessionDelegate = DatadogURLSessionDelegate @available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") public protocol __URLSessionDelegateProviding: URLSessionDelegate { /// Datadog delegate object. - /// + /// /// The class implementing `DDURLSessionDelegateProviding` must ensure that following method calls are forwarded to `ddURLSessionDelegate`: /// - `func urlSession(_:task:didFinishCollecting:)` /// - `func urlSession(_:task:didCompleteWithError:)` diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/ImmutableRequest.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/ImmutableRequest.swift new file mode 100644 index 0000000000..be7951f400 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/ImmutableRequest.swift @@ -0,0 +1,34 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// An immutable version of `URLRequest`. +/// +/// Introduced in response to concerns raised in https://github.com/DataDog/dd-sdk-ios/issues/1638 +/// it makes a copy of request attributes, safeguarding against potential thread safety issues arising from concurrent +/// mutations (see more context in https://github.com/DataDog/dd-sdk-ios/pull/1767 ). +public struct ImmutableRequest { + /// The URL of the request. + public let url: URL? + /// The HTTP method of the request. + public let httpMethod: String? + /// The value of `x-datadog-origin` header (if any). + public let ddOriginHeaderValue: String? + /// A reference to the original `URLRequest` object provided during initialization. Direct use is discouraged + /// due to thread safety concerns. Instead, necessary attributes should be accessed through `ImmutableRequest` fields. + public let unsafeOriginal: URLRequest + + public init(request: URLRequest) { + self.url = request.url + self.httpMethod = request.httpMethod + // RUM-3183: As observed in https://github.com/DataDog/dd-sdk-ios/issues/1638, accessing `request.allHTTPHeaderFields` is not + // safe and can lead to crashes with undefined root cause. To avoid issues we should prefer `request.value(forHTTPHeaderField:)` + // when interacting with `URLRequest`. + self.ddOriginHeaderValue = request.value(forHTTPHeaderField: TracingHTTPHeaders.originField) + self.unsafeOriginal = request + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift index 6892509bfd..66c33ba24e 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift @@ -98,7 +98,7 @@ public struct URLSessionInterceptor { // MARK: - Private private func extractTraceID(from request: URLRequest) -> TraceID? { - guard let headers = request.allHTTPHeaderFields else { + guard let headers = request.allHTTPHeaderFields else { // swiftlint:disable:this unsafe_all_http_header_fields return nil } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift index 22486f7b3b..17bfb3b7bb 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift @@ -81,30 +81,6 @@ public struct ResourceCompletion { } } -/// An immutable version of `URLRequest`. -/// -/// Introduced in response to concerns raised in https://github.com/DataDog/dd-sdk-ios/issues/1638 -/// it makes a copy of request attributes, safeguarding against potential thread safety issues arising from concurrent -/// mutations (see more context in https://github.com/DataDog/dd-sdk-ios/pull/1767 ). -public struct ImmutableRequest { - /// The URL of the request. - public let url: URL? - /// The HTTP method of the request. - public let httpMethod: String? - /// The HTTP header fields of the request. - public let allHTTPHeaderFields: [String: String]? - /// A reference to the original `URLRequest` object provided during initialization. Direct use is discouraged - /// due to thread safety concerns. Instead, necessary attributes should be accessed through `ImmutableRequest` fields. - public let unsafeOriginal: URLRequest - - public init(request: URLRequest) { - self.url = request.url - self.httpMethod = request.httpMethod - self.allHTTPHeaderFields = request.allHTTPHeaderFields - self.unsafeOriginal = request - } -} - /// Encapsulates key metrics retrieved either from `URLSessionTaskMetrics` or any other relevant data source. /// Reference: https://developer.apple.com/documentation/foundation/urlsessiontasktransactionmetrics public struct ResourceMetrics { diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift index 036f07feca..8b88138414 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -39,6 +39,7 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { private let tracestate: [String: String] private let samplingStrategy: TraceSamplingStrategy + private let traceContextInjection: TraceContextInjection /// Initializes the headers writer. /// @@ -55,16 +56,22 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// - 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(samplingStrategy: .custom(sampleRate: sampleRate), tracestate: tracestate) + self.init(samplingStrategy: .custom(sampleRate: sampleRate), tracestate: tracestate, traceContextInjection: .all) } /// Initializes the headers writer. /// /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. /// - Parameter tracestate: The tracestate to be injected. - public init(samplingStrategy: TraceSamplingStrategy, tracestate: [String: String] = [:]) { + /// - Parameter traceContextInjection: The strategy for injecting trace context into requests. + public init( + samplingStrategy: TraceSamplingStrategy, + tracestate: [String: String] = [:], + traceContextInjection: TraceContextInjection = .all + ) { self.samplingStrategy = samplingStrategy self.tracestate = tracestate + self.traceContextInjection = traceContextInjection } /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. @@ -78,28 +85,33 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { let sampler = samplingStrategy.sampler(for: traceContext) let sampled = sampler.sample() - traceHeaderFields[W3CHTTPHeaders.traceparent] = [ - Constants.version, - String(traceContext.traceID, representation: .hexadecimal32Chars), - String(traceContext.spanID, representation: .hexadecimal16Chars), - sampled ? Constants.sampledValue : Constants.unsampledValue - ] - .joined(separator: Constants.separator) + switch (traceContextInjection, sampled) { + case (.all, _), (.sampled, true): + traceHeaderFields[W3CHTTPHeaders.traceparent] = [ + Constants.version, + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), + sampled ? Constants.sampledValue : Constants.unsampledValue + ] + .joined(separator: Constants.separator) - // while merging, the tracestate values from the tracestate property take precedence - // over the ones from the trace context - let tracestate: [String: String] = [ - Constants.sampling: "\(sampled ? 1 : 0)", - Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) - ].merging(tracestate) { old, new in - return new - } + // while merging, the tracestate values from the tracestate property take precedence + // over the ones from the trace context + let tracestate: [String: String] = [ + Constants.sampling: "\(sampled ? 1 : 0)", + Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) + ].merging(tracestate) { old, new in + return new + } - let ddtracestate = tracestate - .map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" } - .sorted() - .joined(separator: Constants.tracestatePairSeparator) + let ddtracestate = tracestate + .map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" } + .sorted() + .joined(separator: Constants.tracestatePairSeparator) - traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)" + traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)" + case (.sampled, false): + break + } } } diff --git a/DatadogInternal/Sources/Utils/SharedMetrics.swift b/DatadogInternal/Sources/SDKMetrics/MethodCalledMetric.swift similarity index 89% rename from DatadogInternal/Sources/Utils/SharedMetrics.swift rename to DatadogInternal/Sources/SDKMetrics/MethodCalledMetric.swift index 61bed76bb4..2d17c5c822 100644 --- a/DatadogInternal/Sources/Utils/SharedMetrics.swift +++ b/DatadogInternal/Sources/SDKMetrics/MethodCalledMetric.swift @@ -6,12 +6,6 @@ import Foundation -/// Common definitions for telemetries. -public enum BasicMetric { - /// Basic Metric type key. - public static let typeKey = "metric_type" -} - /// Definition of "Method Called" telemetry. public enum MethodCalledMetric { /// The name of this metric, included in telemetry log. @@ -57,6 +51,6 @@ public enum MethodCalledMetric { public extension [String: Encodable] { var isMethodCallAttributes: Bool { - self[BasicMetric.typeKey] as? String == MethodCalledMetric.typeValue + self[SDKMetricFields.typeKey] as? String == MethodCalledMetric.typeValue } } diff --git a/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift new file mode 100644 index 0000000000..b2b98b5470 --- /dev/null +++ b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift @@ -0,0 +1,13 @@ +/* + * 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 + +/// Common fields in SDK metrics. +public enum SDKMetricFields { + /// Metric type key. + public static let typeKey = "metric_type" +} diff --git a/DatadogInternal/Sources/Telemetry/Telemetry.swift b/DatadogInternal/Sources/Telemetry/Telemetry.swift index d0150d20a4..5f875d2eeb 100644 --- a/DatadogInternal/Sources/Telemetry/Telemetry.swift +++ b/DatadogInternal/Sources/Telemetry/Telemetry.swift @@ -28,6 +28,8 @@ public struct ConfigurationTelemetry: Equatable { public let startSessionReplayRecordingManually: Bool? public let telemetryConfigurationSampleRate: Int64? public let telemetrySampleRate: Int64? + public let tracerAPI: String? + public let tracerAPIVersion: String? public let traceSampleRate: Int64? public let trackBackgroundEvents: Bool? public let trackCrossPlatformLongTasks: Bool? @@ -57,7 +59,7 @@ public struct ConfigurationTelemetry: Equatable { public enum TelemetryMessage { case debug(id: String, message: String, attributes: [String: Encodable]?) - case error(id: String, message: String, kind: String?, stack: String?) + case error(id: String, message: String, kind: String, stack: String) case configuration(ConfigurationTelemetry) case metric(name: String, attributes: [String: Encodable]) } @@ -126,7 +128,7 @@ public struct MethodCalledTrace { MethodCalledMetric.operationName: operationName, MethodCalledMetric.callerClass: callerClass, MethodCalledMetric.isSuccessful: isSuccessful, - BasicMetric.typeKey: MethodCalledMetric.typeValue + SDKMetricFields.typeKey: MethodCalledMetric.typeValue ] ) } @@ -150,7 +152,7 @@ extension Telemetry { /// - message: The error message. /// - kind: The kind of error. /// - stack: The stack trace. - public func error(id: String, message: String, kind: String?, stack: String?) { + public func error(id: String, message: String, kind: String, stack: String) { send(telemetry: .error(id: id, message: message, kind: kind, stack: stack)) } @@ -171,7 +173,7 @@ extension Telemetry { /// - attributes: Custom attributes attached to the log (optional). /// - file: The current file name. /// - line: The line number in file. - public func debug(_ message: String, attributes: [String: Encodable]? = nil, file: String = #file, line: Int = #line) { + public func debug(_ message: String, attributes: [String: Encodable]? = nil, file: String = #fileID, line: Int = #line) { debug(id: "\(file):\(line):\(message)", message: message, attributes: attributes) } @@ -179,13 +181,17 @@ extension Telemetry { /// /// - Parameters: /// - message: The error message. + /// - kind: The kind of error. /// - stack: The stack trace. /// - file: The current file name. /// - line: The line number in file. - /// - file: The current file name. - /// - line: The line number in file. - public func error(_ message: String, kind: String? = nil, stack: String? = nil, file: String = #file, line: Int = #line) { - error(id: "\(file):\(line):\(message)", message: message, kind: kind, stack: stack) + public func error(_ message: String, kind: String? = nil, stack: String? = nil, file: String = #fileID, line: Int = #line) { + error( + id: "\(file):\(line):\(message)", + message: message, + kind: kind ?? "\(file)", + stack: stack ?? "\(file):\(line)" + ) } /// Collect execution error. @@ -194,7 +200,7 @@ extension Telemetry { /// - error: The error. /// - file: The current file name. /// - line: The line number in file. - public func error(_ error: DDError, file: String = #file, line: Int = #line) { + public func error(_ error: DDError, file: String = #fileID, line: Int = #line) { self.error(error.message, kind: error.type, stack: error.stack, file: file, line: line) } @@ -205,7 +211,7 @@ extension Telemetry { /// - error: The error. /// - file: The current file name. /// - line: The line number in file. - public func error(_ message: String, error: DDError, file: String = #file, line: Int = #line) { + public func error(_ message: String, error: DDError, file: String = #fileID, line: Int = #line) { self.error("\(message) - \(error.message)", kind: error.type, stack: error.stack, file: file, line: line) } @@ -215,7 +221,7 @@ extension Telemetry { /// - error: The error. /// - file: The current file name. /// - line: The line number in file. - public func error(_ error: Error, file: String = #file, line: Int = #line) { + public func error(_ error: Error, file: String = #fileID, line: Int = #line) { self.error(DDError(error: error), file: file, line: line) } @@ -226,7 +232,7 @@ extension Telemetry { /// - error: The error. /// - file: The current file name. /// - line: The line number in file. - public func error(_ message: String, error: Error, file: String = #file, line: Int = #line) { + public func error(_ message: String, error: Error, file: String = #fileID, line: Int = #line) { self.error(message, error: DDError(error: error), file: file, line: line) } @@ -256,6 +262,8 @@ extension Telemetry { startSessionReplayRecordingManually: Bool? = nil, telemetryConfigurationSampleRate: Int64? = nil, telemetrySampleRate: Int64? = nil, + tracerAPI: String? = nil, + tracerAPIVersion: String? = nil, traceSampleRate: Int64? = nil, trackBackgroundEvents: Bool? = nil, trackCrossPlatformLongTasks: Bool? = nil, @@ -304,6 +312,8 @@ extension Telemetry { startSessionReplayRecordingManually: startSessionReplayRecordingManually, telemetryConfigurationSampleRate: telemetryConfigurationSampleRate, telemetrySampleRate: telemetrySampleRate, + tracerAPI: tracerAPI, + tracerAPIVersion: tracerAPIVersion, traceSampleRate: traceSampleRate, trackBackgroundEvents: trackBackgroundEvents, trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, @@ -409,6 +419,8 @@ extension ConfigurationTelemetry { startSessionReplayRecordingManually: other.startSessionReplayRecordingManually ?? startSessionReplayRecordingManually, telemetryConfigurationSampleRate: other.telemetryConfigurationSampleRate ?? telemetryConfigurationSampleRate, telemetrySampleRate: other.telemetrySampleRate ?? telemetrySampleRate, + tracerAPI: other.tracerAPI ?? tracerAPI, + tracerAPIVersion: other.tracerAPIVersion ?? tracerAPIVersion, traceSampleRate: other.traceSampleRate ?? traceSampleRate, trackBackgroundEvents: other.trackBackgroundEvents ?? trackBackgroundEvents, trackCrossPlatformLongTasks: other.trackCrossPlatformLongTasks ?? trackCrossPlatformLongTasks, diff --git a/DatadogInternal/Sources/Upload/Event.swift b/DatadogInternal/Sources/Upload/Event.swift index 6f05f8dc9a..f2ff635f0b 100644 --- a/DatadogInternal/Sources/Upload/Event.swift +++ b/DatadogInternal/Sources/Upload/Event.swift @@ -22,3 +22,9 @@ public struct Event: Equatable { self.metadata = metadata } } + +extension Event: CustomDebugStringConvertible { + public var debugDescription: String { + return .init(data: data, encoding: .utf8) ?? "" + } +} diff --git a/DatadogInternal/Sources/Upload/URLRequestBuilder.swift b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift index 9709795ddb..38f6625fa5 100644 --- a/DatadogInternal/Sources/Upload/URLRequestBuilder.swift +++ b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift @@ -145,7 +145,9 @@ public struct URLRequestBuilder { } } - request.allHTTPHeaderFields = headers + headers.forEach { field, value in + request.setValue(value, forHTTPHeaderField: field) + } return request } } diff --git a/DatadogInternal/Sources/Utils/DateFormatting.swift b/DatadogInternal/Sources/Utils/DateFormatting.swift index 44b33a08cb..7dc4f94cf5 100644 --- a/DatadogInternal/Sources/Utils/DateFormatting.swift +++ b/DatadogInternal/Sources/Utils/DateFormatting.swift @@ -8,6 +8,7 @@ import Foundation public protocol DateFormatterType { func string(from date: Date) -> String + func date(from string: String) -> Date? } extension ISO8601DateFormatter: DateFormatterType {} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift index cec2091c1c..8bfcab0ca9 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift @@ -81,7 +81,7 @@ class B3HTTPHeadersReaderTests: XCTestCase { func testReadingSampledTraceContext() { let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! - let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), injectEncoding: encoding) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), injectEncoding: encoding, traceContextInjection: .all) writer.write(traceContext: .mockRandom()) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -89,13 +89,23 @@ class B3HTTPHeadersReaderTests: XCTestCase { XCTAssertEqual(reader.sampled, true) } - func testReadingNotSampledTraceContext() { + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsAll() { let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! - let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding, traceContextInjection: .all) 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) } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsSampled() { + let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding, traceContextInjection: .sampled) + writer.write(traceContext: .mockRandom()) + + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertNil(reader.sampled) + } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift index 7491012888..74bad2fb05 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift @@ -12,7 +12,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: .all ) writer.write( @@ -31,7 +32,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: .all ) writer.write( @@ -50,7 +52,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withSingleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .custom(sampleRate: 100), - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: .all ) writer.write( @@ -69,7 +72,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withSingleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .custom(sampleRate: 0), - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: .all ) writer.write( @@ -88,7 +92,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testItWritesSingleHeaderWithoutOptionalValues() { let writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: .all ) writer.write( @@ -106,7 +111,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: .all ) writer.write( @@ -128,7 +134,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: .all ) writer.write( @@ -150,7 +157,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .custom(sampleRate: 100), - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: .all ) writer.write( @@ -172,7 +180,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( samplingStrategy: .custom(sampleRate: 0), - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: .all ) writer.write( @@ -194,7 +203,8 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testItWritesMultipleHeaderWithoutOptionalValues() { let writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: .all ) writer.write( diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift index 880aa4f9d3..fc313b729e 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift @@ -10,7 +10,7 @@ import TestUtilities class HTTPHeadersReaderTests: XCTestCase { func testReadingSampledTraceContext() { - let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), traceContextInjection: .all) writer.write(traceContext: .mockRandom()) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -18,12 +18,21 @@ class HTTPHeadersReaderTests: XCTestCase { XCTAssertEqual(reader.sampled, true) } - func testReadingNotSampledTraceContext() { - let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsAll() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .all) writer.write(traceContext: .mockRandom()) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) - XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertNotNil(reader.read(), "When not sampled, it should return no trace context") XCTAssertEqual(reader.sampled, false) } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsSampled() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .sampled) + writer.write(traceContext: .mockRandom()) + + let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertNil(reader.sampled) + } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift index c104331c82..1a526f6ae8 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift @@ -9,8 +9,8 @@ import TestUtilities import DatadogInternal class HTTPHeadersWriterTests: XCTestCase { - func testWritingSampledTraceContext_withAutoSamplingStrategy() { - let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + func testWritingSampledTraceContext_withHeadBasedSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) writer.write( traceContext: .mockWith( @@ -27,8 +27,8 @@ class HTTPHeadersWriterTests: XCTestCase { XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") } - func testWritingDroppedTraceContext_withAutoSamplingStrategy() { - let writer = HTTPHeadersWriter(samplingStrategy: .headBased) + func testWritingDroppedTraceContext_withHeadBasedSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .sampled) writer.write( traceContext: .mockWith( @@ -39,14 +39,14 @@ class HTTPHeadersWriterTests: XCTestCase { ) let headers = writer.traceHeaderFields - XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "0") + XCTAssertNil(headers[TracingHTTPHeaders.samplingPriorityField]) XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) } func testWritingSampledTraceContext_withCustomSamplingStrategy() { - let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), traceContextInjection: .all) writer.write( traceContext: .mockWith( @@ -64,7 +64,7 @@ class HTTPHeadersWriterTests: XCTestCase { } func testWritingDroppedTraceContext_withCustomSamplingStrategy() { - let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .sampled) writer.write( traceContext: .mockWith( @@ -75,7 +75,7 @@ class HTTPHeadersWriterTests: XCTestCase { ) let headers = writer.traceHeaderFields - XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "0") + XCTAssertNil(headers[TracingHTTPHeaders.samplingPriorityField]) XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) diff --git a/DatadogInternal/Tests/NetworkInstrumentation/ImmutableRequestTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/ImmutableRequestTests.swift new file mode 100644 index 0000000000..d805579bcc --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/ImmutableRequestTests.swift @@ -0,0 +1,40 @@ +/* + * 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 ImmutableRequestTests: XCTestCase { + func testReadingURL() { + let original: URLRequest = .mockWith(url: "https://example.com") + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.url, original.url) + } + + func testReadingHTTPMethod() { + let original: URLRequest = .mockWith(httpMethod: .mockRandom()) + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.httpMethod, original.httpMethod) + } + + func testReadingDatadogOriginHeader() { + let expectedValue: String = .mockRandom(length: 128) + let original: URLRequest = .mockWith( + headers: [ + TracingHTTPHeaders.originField: expectedValue + ] + ) + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.ddOriginHeaderValue, expectedValue) + } + + func testPreservingUnsafeOriginal() { + let original: URLRequest = .mockAny() + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.unsafeOriginal, original) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift index df81412ade..40693b5166 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift @@ -19,7 +19,6 @@ class NetworkInstrumentationFeatureTests: XCTestCase { core = SingleFeatureCoreMock() handler = URLSessionHandlerMock() - try core.register(urlSessionHandler: handler) } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift index 6778803612..ffb69e103d 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift @@ -27,7 +27,7 @@ class W3CHTTPHeadersReaderTests: XCTestCase { } func testReadingSampledTraceContext() { - let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), traceContextInjection: .all) writer.write(traceContext: .mockRandom()) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -35,12 +35,21 @@ class W3CHTTPHeadersReaderTests: XCTestCase { XCTAssertEqual(reader.sampled, true) } - func testReadingNotSampledTraceContext() { - let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsAll() { + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .all) 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) } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsSampled() { + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .sampled) + writer.write(traceContext: .mockRandom()) + + let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should not return trace context") + XCTAssertNil(reader.sampled) + } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift index 931714ca3e..598de0fb94 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift @@ -9,12 +9,13 @@ import TestUtilities import DatadogInternal class W3CHTTPHeadersWriterTests: XCTestCase { - func testWritingSampledTraceContext_withAutoSamplingStrategy() { + func testWritingSampledTraceContext_withHeadBasedSamplingStrategy() { let writer = W3CHTTPHeadersWriter( samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] + ], + traceContextInjection: .all ) writer.write( @@ -30,12 +31,13 @@ class W3CHTTPHeadersWriterTests: XCTestCase { XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") } - func testWritingDroppedTraceContext_withAutoSamplingStrategy() { + func testWritingDroppedTraceContext_withHeadBasedSamplingStrategy() { let writer = W3CHTTPHeadersWriter( samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] + ], + traceContextInjection: .all ) writer.write( @@ -57,7 +59,8 @@ class W3CHTTPHeadersWriterTests: XCTestCase { samplingStrategy: .custom(sampleRate: 100), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] + ], + traceContextInjection: .all ) writer.write( @@ -78,7 +81,8 @@ class W3CHTTPHeadersWriterTests: XCTestCase { samplingStrategy: .custom(sampleRate: 0), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] + ], + traceContextInjection: .all ) writer.write( diff --git a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift index 6f068e8a59..3b804681c3 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift @@ -32,6 +32,8 @@ extension ConfigurationTelemetry { startSessionReplayRecordingManually: .mockRandom(), telemetryConfigurationSampleRate: .mockRandom(), telemetrySampleRate: .mockRandom(), + tracerAPI: .mockRandom(), + tracerAPIVersion: .mockRandom(), traceSampleRate: .mockRandom(), trackBackgroundEvents: .mockRandom(), trackCrossPlatformLongTasks: .mockRandom(), diff --git a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift index 49a8da34bc..f61db878ac 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift @@ -5,265 +5,179 @@ */ import XCTest +import Foundation import TestUtilities - @testable import DatadogInternal class TelemetryTests: XCTestCase { - func testTelemetryDebug() { - // Given - class TelemetryTest: Telemetry { - var debug: (id: String, message: String, attributes: [String: Encodable]?)? - - func send(telemetry: DatadogInternal.TelemetryMessage) { - guard case let .debug(id, message, attributes) = telemetry else { - return - } - - debug = (id: id, message: message, attributes: attributes) - } - } + private let telemetry = TelemetryMock() - let telemetry = TelemetryTest() - - struct SwiftError: Error { - let description = "error description" - } + // MARK: - Debug Telemetry + func testSendingDebugTelemetry() throws { // When #sourceLocation(file: "File.swift", line: 1) telemetry.debug("debug message", attributes: ["foo": "bar"]) #sourceLocation() // Then - XCTAssertEqual(telemetry.debug?.id, "File.swift:1:debug message") - XCTAssertEqual(telemetry.debug?.message, "debug message") - XCTAssertEqual(telemetry.debug?.attributes as? [String: String], ["foo": "bar"]) + let debug = try XCTUnwrap(telemetry.messages.firstDebug()) + XCTAssertEqual(debug.id, "\(moduleName())/File.swift:1:debug message") + XCTAssertEqual(debug.message, "debug message") + XCTAssertEqual(debug.attributes as? [String: String], ["foo": "bar"]) + XCTAssertEqual(telemetry.messages.count, 1) } - func testTelemetryErrorFormatting() { - // Given - class TelemetryTest: Telemetry { - var error: (id: String, message: String, kind: String?, stack: String?)? - - func send(telemetry: DatadogInternal.TelemetryMessage) { - guard case let .error(id, message, kind, stack) = telemetry else { - return - } - - error = (id: id, message: message, kind: kind, stack: stack) - } - } - - let telemetry = TelemetryTest() - - struct SwiftError: Error { - let description = "error description" - } - - let swiftError = SwiftError() - - let nsError = NSError( - domain: "custom-domain", - code: 10, - userInfo: [ - NSLocalizedDescriptionKey: "error description" - ] - ) + // MARK: - Error Telemetry + func testSendingErrorTelemetry() throws { // When #sourceLocation(file: "File.swift", line: 1) - telemetry.error(swiftError) + telemetry.error("error message", kind: "error.kind", stack: "error.stack") #sourceLocation() // Then - XCTAssertEqual(telemetry.error?.id, #"File.swift:1:SwiftError(description: "error description")"#) - XCTAssertEqual(telemetry.error?.message, #"SwiftError(description: "error description")"#) - XCTAssertEqual(telemetry.error?.kind, "SwiftError") - XCTAssertEqual(telemetry.error?.stack, #"SwiftError(description: "error description")"#) + let error = try XCTUnwrap(telemetry.messages.firstError()) + XCTAssertEqual(error.id, "\(moduleName())/File.swift:1:error message") + XCTAssertEqual(error.message, "error message") + XCTAssertEqual(error.kind, "error.kind") + XCTAssertEqual(error.stack, "error.stack") + XCTAssertEqual(telemetry.messages.count, 1) + } + func testSendingErrorTelemetry_whenNoKindAndNoStack() throws { // When - #sourceLocation(file: "File.swift", line: 2) - telemetry.error(nsError) + #sourceLocation(file: "File.swift", line: 1) + telemetry.error("error message") #sourceLocation() // Then - XCTAssertEqual(telemetry.error?.id, "File.swift:2:error description") - XCTAssertEqual(telemetry.error?.message, "error description") - XCTAssertEqual(telemetry.error?.kind, "custom-domain - 10") - XCTAssertEqual( - telemetry.error?.stack, - """ - Error Domain=custom-domain Code=10 "error description" UserInfo={NSLocalizedDescription=error description} - """ - ) - - // When - telemetry.error("swift error", error: swiftError) - // Then - XCTAssertEqual(telemetry.error?.message, #"swift error - SwiftError(description: "error description")"#) - - // When - telemetry.error("ns error", error: nsError) - // Then - XCTAssertEqual(telemetry.error?.message, "ns error - error description") + let error = try XCTUnwrap(telemetry.messages.firstError()) + XCTAssertEqual(error.id, "\(moduleName())/File.swift:1:error message") + XCTAssertEqual(error.message, "error message") + XCTAssertEqual(error.kind, "\(moduleName())/File.swift") + XCTAssertEqual(error.stack, "\(moduleName())/File.swift:1") + XCTAssertEqual(telemetry.messages.count, 1) } - func testTelemetryConfiguration() { + func testSendingErrorTelemetry_withSwiftError() throws { // Given - let expectedConfiguration: ConfigurationTelemetry = .mockRandom() - - let telemetry = TelemetryTest() + struct SwiftError: Error { + let description = "error description" + } + let swiftError = SwiftError() // When - telemetry.applyConfiguration(configuration: expectedConfiguration) + telemetry.error(swiftError) + telemetry.error("custom message", error: swiftError) // Then - XCTAssertEqual(telemetry.configuration, expectedConfiguration) + let errors = telemetry.messages.compactMap({ $0.asError }) + XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(errors[0].message, #"SwiftError(description: "error description")"#) + XCTAssertEqual(errors[0].kind, "SwiftError") + XCTAssertEqual(errors[0].stack, #"SwiftError(description: "error description")"#) + XCTAssertEqual(errors[1].message, #"custom message - SwiftError(description: "error description")"#) + XCTAssertEqual(errors[1].kind, "SwiftError") + XCTAssertEqual(errors[1].stack, #"SwiftError(description: "error description")"#) } - func testTelemetryConfigurationMerge() { + func testSendingErrorTelemetry_withNSError() throws { // Given - let initialConfiguration: ConfigurationTelemetry = .mockRandom() - let expectedConfiguration: ConfigurationTelemetry = .mockRandom() - - let telemetry = TelemetryTest() + let nsError = NSError( + domain: "custom-domain", + code: 10, + userInfo: [NSLocalizedDescriptionKey: "error description"] + ) // When - telemetry.applyConfiguration(configuration: initialConfiguration) - telemetry.applyConfiguration(configuration: expectedConfiguration) + telemetry.error(nsError) + telemetry.error("custom message", error: nsError) // Then - XCTAssertEqual(telemetry.configuration, expectedConfiguration) - XCTAssertNotEqual(telemetry.configuration, initialConfiguration) + let errors = telemetry.messages.compactMap({ $0.asError }) + XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(errors[0].message, "error description") + XCTAssertEqual(errors[0].kind, "custom-domain - 10") + XCTAssertEqual(errors[0].stack, #"Error Domain=custom-domain Code=10 "error description" UserInfo={NSLocalizedDescription=error description}"#) + XCTAssertEqual(errors[1].message, "custom message - error description") + XCTAssertEqual(errors[1].kind, "custom-domain - 10") + XCTAssertEqual(errors[1].stack, #"Error Domain=custom-domain Code=10 "error description" UserInfo={NSLocalizedDescription=error description}"#) } - func testWhenSendingTelemetryMessage_itForwardsToCore() throws { - // Given - class Receiver: FeatureMessageReceiver { - var telemetry: TelemetryMessage? - - func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { - guard case .telemetry(let telemetry) = message else { - return false - } - - self.telemetry = telemetry - return true - } - } - - let receiver = Receiver() - let core = PassthroughCoreMock(messageReceiver: receiver) + // MARK: - Configuration Telemetry + func testSendingConfigurationTelemetry() throws { // When - core.telemetry.debug("debug") + telemetry.configuration(batchSize: 123, batchUploadFrequency: 456) // only some values // Then - guard case .debug(_, let message, _) = receiver.telemetry else { - return XCTFail("A debug should be send to core.") - } - XCTAssertEqual(message, "debug") + let configuration = try XCTUnwrap(telemetry.messages.firstConfiguration()) + XCTAssertEqual(configuration.batchSize, 123) + XCTAssertEqual(configuration.batchUploadFrequency, 456) + } + + // MARK: - Metric Telemetry + func testSendingMetricTelemetry() throws { // When - core.telemetry.error("error") + telemetry.metric(name: "metric name", attributes: ["attribute": "value"]) // Then - guard case .error(_, let message, _, _) = receiver.telemetry else { - return XCTFail("An error should be send to core.") - } - XCTAssertEqual(message, "error") + let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) + XCTAssertEqual(metric.name, "metric name") + XCTAssertEqual(metric.attributes as? [String: String], ["attribute": "value"]) + } - // When - core.telemetry.configuration(batchSize: 0) + func testStartingMethodCalledMetricTrace_whenSampled() throws { + XCTAssertNotNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), samplingRate: 100)) + } - // Then - guard case .configuration(let configuration) = receiver.telemetry else { - return XCTFail("An error should be send to core.") - } - XCTAssertEqual(configuration.batchSize, 0) + func testStartingMethodCalledMetricTrace_whenNotSampled() throws { + XCTAssertNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), samplingRate: 0)) + } + + func testTrackingMethodCallMetricTelemetry() throws { + let operationName: String = .mockRandom() + let callerClass: String = .mockRandom() + let isSuccessful: Bool = .random() // When - let operationName = String.mockRandom() - let callerClass = String.mockRandom() - let isSuccessful = Bool.random() - core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: operationName, callerClass: callerClass, samplingRate: 100), - isSuccessful: isSuccessful - ) + let metricTrace = telemetry.startMethodCalled(operationName: operationName, callerClass: callerClass, samplingRate: 100) + Thread.sleep(forTimeInterval: 0.05) + telemetry.stopMethodCalled(metricTrace, isSuccessful: isSuccessful) // Then - guard case .metric(let name, let attributes) = receiver.telemetry else { - return XCTFail("A debug should be send to core.") - } - XCTAssertEqual(name, MethodCalledMetric.name) - XCTAssertGreaterThan(try XCTUnwrap(attributes[MethodCalledMetric.executionTime] as? Int64), 0) - XCTAssertEqual(try XCTUnwrap(attributes[MethodCalledMetric.operationName] as? String), operationName) - XCTAssertEqual(try XCTUnwrap(attributes[MethodCalledMetric.callerClass] as? String), callerClass) - XCTAssertEqual(try XCTUnwrap(attributes[MethodCalledMetric.isSuccessful] as? Bool), isSuccessful) - XCTAssertEqual(try XCTUnwrap(attributes[BasicMetric.typeKey] as? String), MethodCalledMetric.typeValue) + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + XCTAssertEqual(metric.attributes[SDKMetricFields.typeKey] as? String, MethodCalledMetric.typeValue) + XCTAssertEqual(metric.attributes[MethodCalledMetric.operationName] as? String, operationName) + XCTAssertEqual(metric.attributes[MethodCalledMetric.callerClass] as? String, callerClass) + XCTAssertEqual(metric.attributes[MethodCalledMetric.isSuccessful] as? Bool, isSuccessful) + let executionTime = try XCTUnwrap(metric.attributes[MethodCalledMetric.executionTime] as? Int64) + XCTAssertGreaterThan(executionTime, 0) + XCTAssertLessThan(executionTime, TimeInterval(1).toInt64Nanoseconds) } -} -class TelemetryTest: Telemetry { - var configuration: ConfigurationTelemetry? + // MARK: - Integration with Core - func send(telemetry: DatadogInternal.TelemetryMessage) { - guard case .configuration(let configuration) = telemetry else { - return - } + func testWhenUsingCoreTelemetry_itSendsTelemetryToMessageReceiver() throws { + let receiver = FeatureMessageReceiverMock() + let core = PassthroughCoreMock(messageReceiver: receiver) - self.configuration = configuration - } + core.telemetry.debug("debug message") + XCTAssertEqual(receiver.messages.lastTelemetry?.asDebug?.message, "debug message") - internal func applyConfiguration(configuration: ConfigurationTelemetry) { - self.configuration( - actionNameAttribute: configuration.actionNameAttribute, - allowFallbackToLocalStorage: configuration.allowFallbackToLocalStorage, - allowUntrustedEvents: configuration.allowUntrustedEvents, - appHangThreshold: configuration.appHangThreshold, - backgroundTasksEnabled: configuration.backgroundTasksEnabled, - batchProcessingLevel: configuration.batchProcessingLevel, - batchSize: configuration.batchSize, - batchUploadFrequency: configuration.batchUploadFrequency, - dartVersion: configuration.dartVersion, - defaultPrivacyLevel: configuration.defaultPrivacyLevel, - forwardErrorsToLogs: configuration.forwardErrorsToLogs, - initializationType: configuration.initializationType, - mobileVitalsUpdatePeriod: configuration.mobileVitalsUpdatePeriod, - reactNativeVersion: configuration.reactNativeVersion, - reactVersion: configuration.reactVersion, - sessionReplaySampleRate: configuration.sessionReplaySampleRate, - sessionSampleRate: configuration.sessionSampleRate, - silentMultipleInit: configuration.silentMultipleInit, - startSessionReplayRecordingManually: configuration.startSessionReplayRecordingManually, - telemetryConfigurationSampleRate: configuration.telemetryConfigurationSampleRate, - telemetrySampleRate: configuration.telemetrySampleRate, - traceSampleRate: configuration.traceSampleRate, - trackBackgroundEvents: configuration.trackBackgroundEvents, - trackCrossPlatformLongTasks: configuration.trackCrossPlatformLongTasks, - trackErrors: configuration.trackErrors, - trackFlutterPerformance: configuration.trackFlutterPerformance, - trackFrustrations: configuration.trackFrustrations, - trackLongTask: configuration.trackLongTask, - trackNativeErrors: configuration.trackNativeErrors, - trackNativeLongTasks: configuration.trackNativeLongTasks, - trackNativeViews: configuration.trackNativeViews, - trackNetworkRequests: configuration.trackNetworkRequests, - trackResources: configuration.trackResources, - trackSessionAcrossSubdomains: configuration.trackSessionAcrossSubdomains, - trackUserInteractions: configuration.trackUserInteractions, - trackViewsManually: configuration.trackViewsManually, - unityVersion: configuration.unityVersion, - useAllowedTracingUrls: configuration.useAllowedTracingUrls, - useBeforeSend: configuration.useBeforeSend, - useExcludedActivityUrls: configuration.useExcludedActivityUrls, - useFirstPartyHosts: configuration.useFirstPartyHosts, - useLocalEncryption: configuration.useLocalEncryption, - useProxy: configuration.useProxy, - useSecureSessionCookie: configuration.useSecureSessionCookie, - useTracing: configuration.useTracing, - useWorkerUrl: configuration.useWorkerUrl - ) + core.telemetry.error("error message") + XCTAssertEqual(receiver.messages.lastTelemetry?.asError?.message, "error message") + + core.telemetry.configuration(batchSize: 123) + XCTAssertEqual(receiver.messages.lastTelemetry?.asConfiguration?.batchSize, 123) + + core.telemetry.metric(name: "metric name", attributes: [:]) + XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, "metric name") + + let metricTrace = core.telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny()) + core.telemetry.stopMethodCalled(metricTrace) + XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, MethodCalledMetric.name) } } diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec index 7d415c46af..ece57e14c4 100644 --- a/DatadogLogs.podspec +++ b/DatadogLogs.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "DatadogLogs" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Datadog Logs Module." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '11.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } - + s.source_files = ["DatadogLogs/Sources/**/*.swift"] s.dependency 'DatadogInternal', s.version.to_s diff --git a/DatadogLogs/Sources/Feature/Baggages.swift b/DatadogLogs/Sources/Feature/Baggages.swift index 1654768cbb..0f820e1d5f 100644 --- a/DatadogLogs/Sources/Feature/Baggages.swift +++ b/DatadogLogs/Sources/Feature/Baggages.swift @@ -10,6 +10,8 @@ import DatadogInternal /// Error message sent from Logs on the message-bus. internal struct ErrorMessage: Encodable { static let key = "error" + /// The time of the log + let time: Date /// The Log error message let message: String /// The Log error type @@ -20,6 +22,8 @@ internal struct ErrorMessage: Encodable { let source: String = "logger" /// The Log attributes let attributes: AnyEncodable + /// Binary images if need to decode the stack trace + let binaryImages: [BinaryImage]? } internal struct GlobalLogAttributes: Codable { diff --git a/DatadogLogs/Sources/Feature/LogsFeature.swift b/DatadogLogs/Sources/Feature/LogsFeature.swift index 5e5ab84279..ffd1fa1794 100644 --- a/DatadogLogs/Sources/Feature/LogsFeature.swift +++ b/DatadogLogs/Sources/Feature/LogsFeature.swift @@ -16,6 +16,8 @@ internal struct LogsFeature: DatadogRemoteFeature { let logEventMapper: LogEventMapper? + let backtraceReporter: BacktraceReporting? + @ReadWriteLock private var attributes: [String: Encodable] = [:] @@ -26,7 +28,8 @@ internal struct LogsFeature: DatadogRemoteFeature { logEventMapper: LogEventMapper?, dateProvider: DateProvider, customIntakeURL: URL? = nil, - telemetry: Telemetry = NOPTelemetry() + telemetry: Telemetry = NOPTelemetry(), + backtraceReporter: BacktraceReporting? = nil ) { self.init( logEventMapper: logEventMapper, @@ -39,7 +42,8 @@ internal struct LogsFeature: DatadogRemoteFeature { CrashLogReceiver(dateProvider: dateProvider, logEventMapper: logEventMapper), WebViewLogReceiver() ), - dateProvider: dateProvider + dateProvider: dateProvider, + backtraceReporter: backtraceReporter ) } @@ -47,12 +51,14 @@ internal struct LogsFeature: DatadogRemoteFeature { logEventMapper: LogEventMapper?, requestBuilder: FeatureRequestBuilder, messageReceiver: FeatureMessageReceiver, - dateProvider: DateProvider + dateProvider: DateProvider, + backtraceReporter: BacktraceReporting? ) { self.logEventMapper = logEventMapper self.requestBuilder = requestBuilder self.messageReceiver = messageReceiver self.dateProvider = dateProvider + self.backtraceReporter = backtraceReporter } internal func addAttribute(forKey key: AttributeKey, value: AttributeValue) { diff --git a/DatadogLogs/Sources/Feature/MessageReceivers.swift b/DatadogLogs/Sources/Feature/MessageReceivers.swift index 8377c83e7b..a74bc715db 100644 --- a/DatadogLogs/Sources/Feature/MessageReceivers.swift +++ b/DatadogLogs/Sources/Feature/MessageReceivers.swift @@ -69,6 +69,7 @@ internal struct LogMessageReceiver: FeatureMessageReceiver { message: log.message, error: log.error, errorFingerprint: nil, + binaryImages: nil, attributes: .init( userAttributes: log.userAttributes ?? [:], internalAttributes: log.internalAttributes diff --git a/DatadogLogs/Sources/Log/LogEventBuilder.swift b/DatadogLogs/Sources/Log/LogEventBuilder.swift index 34c67853e5..f582829e07 100644 --- a/DatadogLogs/Sources/Log/LogEventBuilder.swift +++ b/DatadogLogs/Sources/Log/LogEventBuilder.swift @@ -33,6 +33,8 @@ internal struct LogEventBuilder { /// - level: the severity level of the log /// - message: the message of the log /// - error: eventual error to associate with log + /// - errorFingerprint: the custom fingerprint for this log + /// - binaryImages: binary images needed to symbolicate the error /// - attributes: attributes to associate with log (user and internal attributes, separate) /// - tags: tags to associate with log /// - context: SDK context from the moment of creating log @@ -46,6 +48,7 @@ internal struct LogEventBuilder { message: String, error: DDError?, errorFingerprint: String?, + binaryImages: [BinaryImage]?, attributes: LogEvent.Attributes, tags: Set, context: DatadogContext, @@ -64,7 +67,8 @@ internal struct LogEventBuilder { message: $0.message, stack: $0.stack, sourceType: $0.sourceType, - fingerprint: errorFingerprint + fingerprint: errorFingerprint, + binaryImages: binaryImages?.toLogDataFormat ) }, serviceName: service, @@ -117,3 +121,20 @@ internal extension LogLevel { } } } + +internal extension BinaryImage { + var toLogDataFormat: LogEvent.Error.BinaryImage { + return .init( + arch: architecture, + isSystem: isSystemLibrary, + loadAddress: loadAddress, + maxAddress: maxAddress, + name: libraryName, + uuid: uuid + ) + } +} + +internal extension Array where Element == BinaryImage { + var toLogDataFormat: [LogEvent.Error.BinaryImage] { map { $0.toLogDataFormat } } +} diff --git a/DatadogLogs/Sources/Log/LogEventEncoder.swift b/DatadogLogs/Sources/Log/LogEventEncoder.swift index 14396b49bc..4d130bef22 100644 --- a/DatadogLogs/Sources/Log/LogEventEncoder.swift +++ b/DatadogLogs/Sources/Log/LogEventEncoder.swift @@ -65,6 +65,36 @@ public struct LogEvent: Encodable { /// Error description associated with a log event. public struct Error { + /// Description of BinaryImage (used for symbolicaiton of stack traces) + public struct BinaryImage: Codable { + /// CPU architecture from the library. + public let arch: String? + + /// Determines if it's a system or user library. + public let isSystem: Bool + + /// Library's load address (hexadecimal). + public let loadAddress: String? + + /// Max value from the library address range (hexadecimal). + public let maxAddress: String? + + /// Name of the library. + public let name: String + + /// Build UUID that uniquely identifies the binary image. + public let uuid: String + + enum CodingKeys: String, CodingKey { + case arch = "arch" + case isSystem = "is_system" + case loadAddress = "load_address" + case maxAddress = "max_address" + case name = "name" + case uuid = "uuid" + } + } + /// The Log error kind public var kind: String? /// The Log error message @@ -75,6 +105,8 @@ public struct LogEvent: Encodable { public var sourceType: String = "ios" /// The custom fingerprint supplied for this error, if any public var fingerprint: String? + /// Binary images needed to decode the provided stack (if any) + public var binaryImages: [BinaryImage]? } /// Device information. @@ -174,6 +206,7 @@ internal struct LogEventEncoder { case errorStack = "error.stack" case errorSourceType = "error.source_type" case errorFingerprint = "error.fingerprint" + case errorBinaryImages = "error.binary_images" // MARK: - Application info @@ -237,6 +270,9 @@ internal struct LogEventEncoder { try container.encode(someError.stack, forKey: .errorStack) try container.encode(someError.sourceType, forKey: .errorSourceType) try container.encode(someError.fingerprint, forKey: .errorFingerprint) + if let binaryImages = someError.binaryImages { + try container.encode(binaryImages, forKey: .errorBinaryImages) + } } // Encode logger info diff --git a/DatadogLogs/Sources/RemoteLogger.swift b/DatadogLogs/Sources/RemoteLogger.swift index e3803d7aff..1506defdf7 100644 --- a/DatadogLogs/Sources/RemoteLogger.swift +++ b/DatadogLogs/Sources/RemoteLogger.swift @@ -100,7 +100,9 @@ internal final class RemoteLogger: LoggerProtocol { return } - let globalAttributes = self.core?.get(feature: LogsFeature.self)?.getAttributes() + let logsFeature = self.core?.get(feature: LogsFeature.self) + + let globalAttributes = logsFeature?.getAttributes() // on user thread: let date = dateProvider.now @@ -111,6 +113,7 @@ internal final class RemoteLogger: LoggerProtocol { var logAttributes = attributes let isCrash = logAttributes?.removeValue(forKey: CrossPlatformAttributes.errorLogIsCrash) as? Bool ?? false let errorFingerprint = logAttributes?.removeValue(forKey: Logs.Attributes.errorFingerprint) as? String + let addBinaryImages = logAttributes?.removeValue(forKey: CrossPlatformAttributes.includeBinaryImages) as? Bool ?? false let userAttributes = self.attributes .merging(logAttributes ?? [:]) { $1 } // prefer message attributes let combinedAttributes: [String: any Encodable] @@ -151,6 +154,13 @@ internal final class RemoteLogger: LoggerProtocol { } } + // When binary images are requested, add them + var binaryImages: [BinaryImage]? + if addBinaryImages { + // TODO: RUM-4072 Replace full backtrace reporter with simpler binary image fetcher + binaryImages = try? logsFeature?.backtraceReporter?.generateBacktrace()?.binaryImages + } + let builder = LogEventBuilder( service: self.configuration.service ?? context.service, loggerName: self.configuration.name, @@ -164,6 +174,7 @@ internal final class RemoteLogger: LoggerProtocol { message: message, error: error, errorFingerprint: errorFingerprint, + binaryImages: binaryImages, attributes: .init( userAttributes: combinedAttributes, internalAttributes: internalAttributes @@ -182,10 +193,12 @@ internal final class RemoteLogger: LoggerProtocol { message: .baggage( key: ErrorMessage.key, value: ErrorMessage( + time: date, message: log.error?.message ?? log.message, type: log.error?.kind, stack: log.error?.stack, - attributes: .init(combinedAttributes) + attributes: .init(combinedAttributes), + binaryImages: binaryImages ) ) ) diff --git a/DatadogLogs/Tests/Log/LogEventBuilderTests.swift b/DatadogLogs/Tests/Log/LogEventBuilderTests.swift index f8d9de0ca1..df8c6c1dd5 100644 --- a/DatadogLogs/Tests/Log/LogEventBuilderTests.swift +++ b/DatadogLogs/Tests/Log/LogEventBuilderTests.swift @@ -45,6 +45,7 @@ class LogEventBuilderTests: XCTestCase { message: randomMessage, error: randomError, errorFingerprint: randomErrorFingerprint, + binaryImages: .mockAny(), attributes: randomAttributes, tags: randomTags, context: .mockWith( @@ -137,6 +138,7 @@ class LogEventBuilderTests: XCTestCase { message: .mockAny(), error: .mockAny(), errorFingerprint: .mockAny(), + binaryImages: .mockAny(), attributes: .mockAny(), tags: .mockAny(), context: randomSDKContext, @@ -187,6 +189,7 @@ class LogEventBuilderTests: XCTestCase { message: .mockAny(), error: .mockAny(), errorFingerprint: .mockAny(), + binaryImages: .mockAny(), attributes: .mockAny(), tags: .mockAny(), context: randomSDKContext, @@ -214,6 +217,7 @@ class LogEventBuilderTests: XCTestCase { message: .mockAny(), error: .mockAny(), errorFingerprint: .mockAny(), + binaryImages: .mockAny(), attributes: .mockAny(), tags: .mockAny(), context: .mockWith( @@ -255,6 +259,7 @@ class LogEventBuilderTests: XCTestCase { message: "original message", error: .mockAny(), errorFingerprint: .mockAny(), + binaryImages: .mockAny(), attributes: .mockAny(), tags: .mockAny(), context: .mockAny(), @@ -289,6 +294,7 @@ class LogEventBuilderTests: XCTestCase { message: .mockAny(), error: .mockAny(), errorFingerprint: .mockAny(), + binaryImages: .mockAny(), attributes: .mockAny(), tags: .mockAny(), context: .mockAny(), diff --git a/DatadogLogs/Tests/LogsTests.swift b/DatadogLogs/Tests/LogsTests.swift index 6d9135b41c..72c202dfce 100644 --- a/DatadogLogs/Tests/LogsTests.swift +++ b/DatadogLogs/Tests/LogsTests.swift @@ -136,7 +136,7 @@ class LogsTests: XCTestCase { XCTAssertEqual((baggage.attributes[attributeKey] as? AnyCodable)?.value as? String, attributeValue) } - func testItSendsGlobalLogUpdates_whenRemovettribute() throws { + func testItSendsGlobalLogUpdates_whenRemoveAttribute() throws { // Given let mockMessageReciever = FeatureMessageReceiverMock() let core = SingleFeatureCoreMock( diff --git a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift index 865fb5fb31..8040d4f5e3 100644 --- a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift +++ b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift @@ -42,13 +42,15 @@ extension LogsFeature { sampler: Sampler = .mockKeepAll(), requestBuilder: FeatureRequestBuilder = RequestBuilder(), messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), - dateProvider: DateProvider = SystemDateProvider() + dateProvider: DateProvider = SystemDateProvider(), + backtraceReporter: BacktraceReporting = BacktraceReporterMock(backtrace: nil) ) -> Self { return .init( logEventMapper: logEventMapper, requestBuilder: requestBuilder, messageReceiver: messageReceiver, - dateProvider: dateProvider + dateProvider: dateProvider, + backtraceReporter: backtraceReporter ) } } diff --git a/DatadogLogs/Tests/RemoteLoggerTests.swift b/DatadogLogs/Tests/RemoteLoggerTests.swift index b799049e16..9896fb042d 100644 --- a/DatadogLogs/Tests/RemoteLoggerTests.swift +++ b/DatadogLogs/Tests/RemoteLoggerTests.swift @@ -260,6 +260,48 @@ class RemoteLoggerTests: XCTestCase { XCTAssertEqual(log.error?.fingerprint, randomErrorFingerprint) } + func testWhenAttributesContainIncludeBinaryImages_itAddsBinaryImagesToLogEvent() throws { + let stubBacktrace: BacktraceReport = .mockRandom() + let logsFeature = LogsFeature.mockWith( + backtraceReporter: BacktraceReporterMock(backtrace: stubBacktrace) + ) + let core = SingleFeatureCoreMock( + feature: logsFeature, + expectation: expectation(description: "Send log") + ) + let logger = RemoteLogger( + core: core, + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false + ) + + // When + logger.error("Information message", error: ErrorMock(), attributes: [CrossPlatformAttributes.includeBinaryImages: true]) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let logs = core.events(ofType: LogEvent.self) + XCTAssertEqual(logs.count, 1) + + let log = try XCTUnwrap(logs.first) + XCTAssertNil(log.attributes.userAttributes[CrossPlatformAttributes.includeBinaryImages]) + XCTAssertNotNil(log.error?.binaryImages) + XCTAssertEqual(log.error?.binaryImages?.count, stubBacktrace.binaryImages.count) + for i in 0.. "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", diff --git a/DatadogObjc/Sources/RUM/RUM+objc.swift b/DatadogObjc/Sources/RUM/RUM+objc.swift index 52f058c2c4..f86218a3d9 100644 --- a/DatadogObjc/Sources/RUM/RUM+objc.swift +++ b/DatadogObjc/Sources/RUM/RUM+objc.swift @@ -452,6 +452,16 @@ public class DDRUMMonitor: NSObject { DDRUMMonitor(swiftRUMMonitor: RUMMonitor.shared()) } + @objc + public func currentSessionID(completion: @escaping (String?) -> Void) { + swiftRUMMonitor.currentSessionID(completion: completion) + } + + @objc + public func stopSession() { + swiftRUMMonitor.stopSession() + } + @objc public func startView( viewController: UIViewController, @@ -633,4 +643,14 @@ public class DDRUMMonitor: NSObject { public func removeAttribute(forKey key: String) { swiftRUMMonitor.removeAttribute(forKey: key) } + + @objc + public func addFeatureFlagEvaluation(name: String, value: Any) { + swiftRUMMonitor.addFeatureFlagEvaluation(name: name, value: AnyEncodable(value)) + } + + @objc public var debug: Bool { + set { swiftRUMMonitor.debug = newValue } + get { swiftRUMMonitor.debug } + } } diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index b803a616f4..3ca5d74aa9 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -1612,6 +1612,10 @@ public class DDRUMErrorEventError: NSObject { get { root.swiftModel.error.causes?.map { DDRUMErrorEventErrorCauses(swiftModel: $0) } } } + @objc public var csp: DDRUMErrorEventErrorCSP? { + root.swiftModel.error.csp != nil ? DDRUMErrorEventErrorCSP(root: root) : nil + } + @objc public var fingerprint: String? { set { root.swiftModel.error.fingerprint = newValue } get { root.swiftModel.error.fingerprint } @@ -1663,6 +1667,10 @@ public class DDRUMErrorEventError: NSObject { root.swiftModel.error.threads?.map { DDRUMErrorEventErrorThreads(swiftModel: $0) } } + @objc public var timeSinceAppStart: NSNumber? { + root.swiftModel.error.timeSinceAppStart as NSNumber? + } + @objc public var type: String? { root.swiftModel.error.type } @@ -1798,6 +1806,42 @@ public enum DDRUMErrorEventErrorCausesSource: Int { case report } +@objc +public class DDRUMErrorEventErrorCSP: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var disposition: DDRUMErrorEventErrorCSPDisposition { + .init(swift: root.swiftModel.error.csp!.disposition) + } +} + +@objc +public enum DDRUMErrorEventErrorCSPDisposition: Int { + internal init(swift: RUMErrorEvent.Error.CSP.Disposition?) { + switch swift { + case nil: self = .none + case .enforce?: self = .enforce + case .report?: self = .report + } + } + + internal var toSwift: RUMErrorEvent.Error.CSP.Disposition? { + switch self { + case .none: return nil + case .enforce: return .enforce + case .report: return .report + } + } + + case none + case enforce + case report +} + @objc public enum DDRUMErrorEventErrorHandling: Int { internal init(swift: RUMErrorEvent.Error.Handling?) { @@ -3763,6 +3807,10 @@ public class DDRUMResourceEventResource: NSObject { root.swiftModel.resource.connect != nil ? DDRUMResourceEventResourceConnect(root: root) : nil } + @objc public var decodedBodySize: NSNumber? { + root.swiftModel.resource.decodedBodySize as NSNumber? + } + @objc public var dns: DDRUMResourceEventResourceDNS? { root.swiftModel.resource.dns != nil ? DDRUMResourceEventResourceDNS(root: root) : nil } @@ -3775,6 +3823,10 @@ public class DDRUMResourceEventResource: NSObject { root.swiftModel.resource.duration as NSNumber? } + @objc public var encodedBodySize: NSNumber? { + root.swiftModel.resource.encodedBodySize as NSNumber? + } + @objc public var firstByte: DDRUMResourceEventResourceFirstByte? { root.swiftModel.resource.firstByte != nil ? DDRUMResourceEventResourceFirstByte(root: root) : nil } @@ -3799,6 +3851,10 @@ public class DDRUMResourceEventResource: NSObject { root.swiftModel.resource.redirect != nil ? DDRUMResourceEventResourceRedirect(root: root) : nil } + @objc public var renderBlockingStatus: DDRUMResourceEventResourceRenderBlockingStatus { + .init(swift: root.swiftModel.resource.renderBlockingStatus) + } + @objc public var size: NSNumber? { root.swiftModel.resource.size as NSNumber? } @@ -3811,6 +3867,10 @@ public class DDRUMResourceEventResource: NSObject { root.swiftModel.resource.statusCode as NSNumber? } + @objc public var transferSize: NSNumber? { + root.swiftModel.resource.transferSize as NSNumber? + } + @objc public var type: DDRUMResourceEventResourceResourceType { .init(swift: root.swiftModel.resource.type) } @@ -4080,6 +4140,29 @@ public class DDRUMResourceEventResourceRedirect: NSObject { } } +@objc +public enum DDRUMResourceEventResourceRenderBlockingStatus: Int { + internal init(swift: RUMResourceEvent.Resource.RenderBlockingStatus?) { + switch swift { + case nil: self = .none + case .blocking?: self = .blocking + case .nonBlocking?: self = .nonBlocking + } + } + + internal var toSwift: RUMResourceEvent.Resource.RenderBlockingStatus? { + switch self { + case .none: return nil + case .blocking: return .blocking + case .nonBlocking: return .nonBlocking + } + } + + case none + case blocking + case nonBlocking +} + @objc public class DDRUMResourceEventResourceSSL: NSObject { internal let root: DDRUMResourceEvent @@ -6931,6 +7014,10 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.batchUploadFrequency as NSNumber? } + @objc public var compressIntakeRequests: NSNumber? { + root.swiftModel.telemetry.configuration.compressIntakeRequests as NSNumber? + } + @objc public var dartVersion: String? { set { root.swiftModel.telemetry.configuration.dartVersion = newValue } get { root.swiftModel.telemetry.configuration.dartVersion } @@ -7015,10 +7102,29 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.telemetrySampleRate as NSNumber? } + @objc public var telemetryUsageSampleRate: NSNumber? { + root.swiftModel.telemetry.configuration.telemetryUsageSampleRate as NSNumber? + } + + @objc public var traceContextInjection: DDTelemetryConfigurationEventTelemetryConfigurationTraceContextInjection { + set { root.swiftModel.telemetry.configuration.traceContextInjection = newValue.toSwift } + get { .init(swift: root.swiftModel.telemetry.configuration.traceContextInjection) } + } + @objc public var traceSampleRate: NSNumber? { root.swiftModel.telemetry.configuration.traceSampleRate as NSNumber? } + @objc public var tracerApi: String? { + set { root.swiftModel.telemetry.configuration.tracerApi = newValue } + get { root.swiftModel.telemetry.configuration.tracerApi } + } + + @objc public var tracerApiVersion: String? { + set { root.swiftModel.telemetry.configuration.tracerApiVersion = newValue } + get { root.swiftModel.telemetry.configuration.tracerApiVersion } + } + @objc public var trackBackgroundEvents: NSNumber? { set { root.swiftModel.telemetry.configuration.trackBackgroundEvents = newValue?.boolValue } get { root.swiftModel.telemetry.configuration.trackBackgroundEvents as NSNumber? } @@ -7093,6 +7199,10 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { get { root.swiftModel.telemetry.configuration.trackViewsManually as NSNumber? } } + @objc public var trackingConsent: String? { + root.swiftModel.telemetry.configuration.trackingConsent + } + @objc public var unityVersion: String? { set { root.swiftModel.telemetry.configuration.unityVersion = newValue } get { root.swiftModel.telemetry.configuration.unityVersion } @@ -7131,6 +7241,11 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.usePartitionedCrossSiteSessionCookie as NSNumber? } + @objc public var usePciIntake: NSNumber? { + set { root.swiftModel.telemetry.configuration.usePciIntake = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.usePciIntake as NSNumber? } + } + @objc public var useProxy: NSNumber? { set { root.swiftModel.telemetry.configuration.useProxy = newValue?.boolValue } get { root.swiftModel.telemetry.configuration.useProxy as NSNumber? } @@ -7228,6 +7343,29 @@ public enum DDTelemetryConfigurationEventTelemetryConfigurationSelectedTracingPr case tracecontext } +@objc +public enum DDTelemetryConfigurationEventTelemetryConfigurationTraceContextInjection: Int { + internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.TraceContextInjection?) { + switch swift { + case nil: self = .none + case .all?: self = .all + case .sampled?: self = .sampled + } + } + + internal var toSwift: TelemetryConfigurationEvent.Telemetry.Configuration.TraceContextInjection? { + switch self { + case .none: return nil + case .all: return .all + case .sampled: return .sampled + } + } + + case none + case all + case sampled +} + @objc public enum DDTelemetryConfigurationEventTelemetryConfigurationViewTrackingStrategy: Int { internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.ViewTrackingStrategy?) { @@ -7272,4 +7410,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/63842bcc15dec58b68fc0a57260715f8dfcde330 +// Generated from https://github.com/DataDog/rum-events-format/tree/0455e104863c0f67c3bf69899c7d5da1ba6f0ebb diff --git a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift index b55ab262c9..3919197e4d 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift @@ -53,18 +53,21 @@ public class DDB3HTTPHeadersWriter: NSObject { ) { swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( samplingStrategy: .custom(sampleRate: sampleRate), - injectEncoding: .init(injectEncoding) + injectEncoding: .init(injectEncoding), + traceContextInjection: .all ) } @objc public init( samplingStrategy: DDTraceSamplingStrategy, - injectEncoding: DDInjectEncoding = .single + injectEncoding: DDInjectEncoding = .single, + traceContextInjection: DDTraceContextInjection = .all ) { swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( samplingStrategy: samplingStrategy.swiftType, - injectEncoding: .init(injectEncoding) + injectEncoding: .init(injectEncoding), + traceContextInjection: traceContextInjection.swiftType ) } } diff --git a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift index 24d759f25f..c5e9ccbf1e 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift @@ -25,14 +25,19 @@ public class DDHTTPHeadersWriter: NSObject { @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( - samplingStrategy: .custom(sampleRate: sampleRate) + samplingStrategy: .custom(sampleRate: sampleRate), + traceContextInjection: .all ) } @objc - public init(samplingStrategy: DDTraceSamplingStrategy) { + public init( + samplingStrategy: DDTraceSamplingStrategy, + traceContextInjection: DDTraceContextInjection + ) { swiftHTTPHeadersWriter = HTTPHeadersWriter( - samplingStrategy: samplingStrategy.swiftType + samplingStrategy: samplingStrategy.swiftType, + traceContextInjection: traceContextInjection.swiftType ) } } diff --git a/DatadogObjc/Sources/Tracing/Propagation/TraceContextInjection+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/TraceContextInjection+objc.swift new file mode 100644 index 0000000000..829e5f92ac --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/TraceContextInjection+objc.swift @@ -0,0 +1,27 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Defines whether the trace context should be injected into all requests or only sampled ones. +@objc +public enum DDTraceContextInjection: Int { + internal var swiftType: DatadogInternal.TraceContextInjection { + switch self { + case .all: + return .all + case .sampled: + return .sampled + } + } + + /// Injects trace context into all requests irrespective of the sampling decision. + case all + + /// Injects trace context only into sampled requests. + case sampled +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift index 1ef16bc401..13841a2c7f 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift @@ -26,17 +26,20 @@ public class DDW3CHTTPHeadersWriter: NSObject { public init(sampleRate: Float = 20) { swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( samplingStrategy: .custom(sampleRate: sampleRate), - tracestate: [:] + tracestate: [:], + traceContextInjection: .all ) } @objc public init( - samplingStrategy: DDTraceSamplingStrategy + samplingStrategy: DDTraceSamplingStrategy, + traceContextInjection: DDTraceContextInjection ) { swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( samplingStrategy: samplingStrategy.swiftType, - tracestate: [:] + tracestate: [:], + traceContextInjection: traceContextInjection.swiftType ) } } diff --git a/DatadogRUM.podspec b/DatadogRUM.podspec index b75f260ac2..f564323599 100644 --- a/DatadogRUM.podspec +++ b/DatadogRUM.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "DatadogRUM" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Datadog Real User Monitoring Module." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '11.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } - + s.source_files = ["DatadogRUM/Sources/**/*.swift"] s.resource_bundle = { diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift index efbae887d6..e9dc6dfe1c 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -698,6 +698,9 @@ public struct RUMErrorEvent: RUMDataModel { /// Causes of the error public var causes: [Causes]? + /// Content Security Violation properties + public let csp: CSP? + /// Fingerprint used for Error Tracking custom grouping public var fingerprint: String? @@ -734,6 +737,9 @@ public struct RUMErrorEvent: RUMDataModel { /// Description of each thread in the process when error happened. public let threads: [Threads]? + /// Time since application start when error happened (in milliseconds) + public let timeSinceAppStart: Int64? + /// The type of the error public let type: String? @@ -744,6 +750,7 @@ public struct RUMErrorEvent: RUMDataModel { case binaryImages = "binary_images" case category = "category" case causes = "causes" + case csp = "csp" case fingerprint = "fingerprint" case handling = "handling" case handlingStack = "handling_stack" @@ -756,6 +763,7 @@ public struct RUMErrorEvent: RUMDataModel { case sourceType = "source_type" case stack = "stack" case threads = "threads" + case timeSinceAppStart = "time_since_app_start" case type = "type" case wasTruncated = "was_truncated" } @@ -831,6 +839,22 @@ public struct RUMErrorEvent: RUMDataModel { } } + /// Content Security Violation properties + public struct CSP: Codable { + /// In the context of CSP errors, indicates how the violated policy is configured to be treated by the user agent. + public let disposition: Disposition? + + enum CodingKeys: String, CodingKey { + case disposition = "disposition" + } + + /// In the context of CSP errors, indicates how the violated policy is configured to be treated by the user agent. + public enum Disposition: String, Codable { + case enforce = "enforce" + case report = "report" + } + } + /// Whether the error has been handled manually in the source code or not public enum Handling: String, Codable { case handled = "handled" @@ -1623,6 +1647,9 @@ public struct RUMResourceEvent: RUMDataModel { /// Connect phase properties public let connect: Connect? + /// Size in octet of the resource after removing any applied encoding + public let decodedBodySize: Int64? + /// DNS phase properties public let dns: DNS? @@ -1632,6 +1659,9 @@ public struct RUMResourceEvent: RUMDataModel { /// Duration of the resource public let duration: Int64? + /// Size in octet of the resource before removing any applied content encodings + public let encodedBodySize: Int64? + /// First Byte phase properties public let firstByte: FirstByte? @@ -1650,6 +1680,9 @@ public struct RUMResourceEvent: RUMDataModel { /// Redirect phase properties public let redirect: Redirect? + /// Render blocking status of the resource + public let renderBlockingStatus: RenderBlockingStatus? + /// Size in octet of the resource response body public let size: Int64? @@ -1659,6 +1692,9 @@ public struct RUMResourceEvent: RUMDataModel { /// HTTP status code of the resource public let statusCode: Int64? + /// Size in octet of the fetched resource + public let transferSize: Int64? + /// Resource type public let type: ResourceType @@ -1667,18 +1703,22 @@ public struct RUMResourceEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case connect = "connect" + case decodedBodySize = "decoded_body_size" case dns = "dns" case download = "download" case duration = "duration" + case encodedBodySize = "encoded_body_size" case firstByte = "first_byte" case graphql = "graphql" case id = "id" case method = "method" case provider = "provider" case redirect = "redirect" + case renderBlockingStatus = "render_blocking_status" case size = "size" case ssl = "ssl" case statusCode = "status_code" + case transferSize = "transfer_size" case type = "type" case url = "url" } @@ -1818,6 +1858,12 @@ public struct RUMResourceEvent: RUMDataModel { } } + /// Render blocking status of the resource + public enum RenderBlockingStatus: String, Codable { + case blocking = "blocking" + case nonBlocking = "non-blocking" + } + /// SSL phase properties public struct SSL: Codable { /// Duration in ns of the resource ssl phase @@ -3364,7 +3410,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Whether UIApplication background tasks are enabled public let backgroundTasksEnabled: Bool? - /// Maximum number of batches processed sequencially without a delay + /// Maximum number of batches processed sequentially without a delay public let batchProcessingLevel: Int64? /// The window duration for batches sent by the SDK (in milliseconds) @@ -3373,6 +3419,9 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// The upload frequency of batches (in milliseconds) public let batchUploadFrequency: Int64? + /// Whether intake requests are compressed + public let compressIntakeRequests: Bool? + /// The version of Dart used in a Flutter application public var dartVersion: String? @@ -3430,9 +3479,21 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// The percentage of telemetry events sent public let telemetrySampleRate: Int64? + /// The percentage of telemetry usage events sent after being sampled by telemetry_sample_rate + public let telemetryUsageSampleRate: Int64? + + /// The opt-in configuration to add trace context + public var traceContextInjection: TraceContextInjection? + /// The percentage of requests traced public let traceSampleRate: Int64? + /// The tracer API used by the SDK. Possible values: 'Datadog', 'OpenTelemetry', 'OpenTracing' + public var tracerApi: String? + + /// The version of the tracer API used by the SDK. Eg. '0.1.0' + public var tracerApiVersion: String? + /// Whether RUM events are tracked when the application is in Background public var trackBackgroundEvents: Bool? @@ -3478,6 +3539,9 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Whether the RUM views creation is handled manually public var trackViewsManually: Bool? + /// The initial tracking consent value + public let trackingConsent: String? + /// The version of Unity used in a Unity application public var unityVersion: String? @@ -3505,6 +3569,9 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Whether a partitioned secure cross-site session cookie is used public let usePartitionedCrossSiteSessionCookie: Bool? + /// Whether logs are sent to the PCI-compliant intake + public var usePciIntake: Bool? + /// Whether a proxy is used public var useProxy: Bool? @@ -3529,6 +3596,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case batchProcessingLevel = "batch_processing_level" case batchSize = "batch_size" case batchUploadFrequency = "batch_upload_frequency" + case compressIntakeRequests = "compress_intake_requests" case dartVersion = "dart_version" case defaultPrivacyLevel = "default_privacy_level" case forwardConsoleLogs = "forward_console_logs" @@ -3548,7 +3616,11 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case storeContextsAcrossPages = "store_contexts_across_pages" case telemetryConfigurationSampleRate = "telemetry_configuration_sample_rate" case telemetrySampleRate = "telemetry_sample_rate" + case telemetryUsageSampleRate = "telemetry_usage_sample_rate" + case traceContextInjection = "trace_context_injection" case traceSampleRate = "trace_sample_rate" + case tracerApi = "tracer_api" + case tracerApiVersion = "tracer_api_version" case trackBackgroundEvents = "track_background_events" case trackCrossPlatformLongTasks = "track_cross_platform_long_tasks" case trackErrors = "track_errors" @@ -3564,6 +3636,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case trackSessionAcrossSubdomains = "track_session_across_subdomains" case trackUserInteractions = "track_user_interactions" case trackViewsManually = "track_views_manually" + case trackingConsent = "tracking_consent" case unityVersion = "unity_version" case useAllowedTracingOrigins = "use_allowed_tracing_origins" case useAllowedTracingUrls = "use_allowed_tracing_urls" @@ -3573,6 +3646,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case useFirstPartyHosts = "use_first_party_hosts" case useLocalEncryption = "use_local_encryption" case usePartitionedCrossSiteSessionCookie = "use_partitioned_cross_site_session_cookie" + case usePciIntake = "use_pci_intake" case useProxy = "use_proxy" case useSecureSessionCookie = "use_secure_session_cookie" case useTracing = "use_tracing" @@ -3671,6 +3745,12 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case tracecontext = "tracecontext" } + /// The opt-in configuration to add trace context + public enum TraceContextInjection: String, Codable { + case all = "all" + case sampled = "sampled" + } + /// View tracking strategy public enum ViewTrackingStrategy: String, Codable { case activityViewTrackingStrategy = "ActivityViewTrackingStrategy" @@ -4001,4 +4081,4 @@ public enum RUMMethod: String, Codable { case connect = "CONNECT" } -// Generated from https://github.com/DataDog/rum-events-format/tree/63842bcc15dec58b68fc0a57260715f8dfcde330 +// Generated from https://github.com/DataDog/rum-events-format/tree/0455e104863c0f67c3bf69899c7d5da1ba6f0ebb diff --git a/DatadogRUM/Sources/Debugging/RUMDebugging.swift b/DatadogRUM/Sources/Debugging/RUMDebugging.swift index 28d8b94b8f..539171757d 100644 --- a/DatadogRUM/Sources/Debugging/RUMDebugging.swift +++ b/DatadogRUM/Sources/Debugging/RUMDebugging.swift @@ -28,11 +28,8 @@ private struct RUMDebugInfo { } internal class RUMDebugging { - private lazy var canvas: UIView = { - let view = RUMDebugView(frame: .zero) - view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - return view - }() + /// An overlay view renderd on top of the app content. It is created lazily on first draw. + private var canvas: UIView? = nil // MARK: - Initialization @@ -52,9 +49,8 @@ internal class RUMDebugging { } deinit { - let canvas = self.canvas - DispatchQueue.main.async { - canvas.removeFromSuperview() + DispatchQueue.main.async { [weak canvas] in + canvas?.removeFromSuperview() UIDevice.current.endGeneratingDeviceOrientationNotifications() } @@ -69,9 +65,8 @@ internal class RUMDebugging { } deinit { - let canvas = self.canvas - DispatchQueue.main.async { - canvas.removeFromSuperview() + DispatchQueue.main.async { [weak canvas] in + canvas?.removeFromSuperview() } } #endif @@ -91,6 +86,15 @@ internal class RUMDebugging { // MARK: - Private private func renderOnMainThread(rumDebugInfo: RUMDebugInfo) { + if canvas == nil { + canvas = RUMDebugView(frame: .zero) + canvas?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + } + + guard let canvas = canvas else { + return + } + canvas.subviews.forEach { view in view.removeFromSuperview() } @@ -118,7 +122,7 @@ internal class RUMDebugging { @objc private func updateLayout() { - canvas.subviews.forEach { $0.setNeedsLayout() } + canvas?.subviews.forEach { $0.setNeedsLayout() } } } diff --git a/DatadogRUM/Sources/FatalErrorBuilder.swift b/DatadogRUM/Sources/FatalErrorBuilder.swift index 446bd224c8..98911fa78c 100644 --- a/DatadogRUM/Sources/FatalErrorBuilder.swift +++ b/DatadogRUM/Sources/FatalErrorBuilder.swift @@ -40,9 +40,12 @@ internal struct FatalErrorBuilder { let errorBinaryImages: [RUMErrorEvent.Error.BinaryImages]? let errorWasTruncated: Bool? let errorMeta: RUMErrorEvent.Error.Meta? + var timeSinceAppStart: TimeInterval? /// Creates RUM error linked to given view. func createRUMError(with lastRUMView: RUMViewEvent) -> RUMErrorEvent { + let msSinceAppStart = timeSinceAppStart.map { max(0, $0.toInt64Milliseconds) } + let event = RUMErrorEvent( dd: .init( browserSdkVersion: nil, @@ -76,6 +79,7 @@ internal struct FatalErrorBuilder { case .hang: return .appHang } }(), + csp: nil, handling: nil, handlingStack: nil, id: nil, @@ -92,6 +96,7 @@ internal struct FatalErrorBuilder { sourceType: context.nativeSourceOverride.map { RUMErrorSourceType(rawValue: $0) } ?? .ios, stack: errorStack, threads: errorThreads, + timeSinceAppStart: msSinceAppStart, type: errorType, wasTruncated: errorWasTruncated ), diff --git a/DatadogRUM/Sources/Feature/RUMBaggageKeys.swift b/DatadogRUM/Sources/Feature/RUMBaggageKeys.swift index 57dbbcd251..f95db7f82d 100644 --- a/DatadogRUM/Sources/Feature/RUMBaggageKeys.swift +++ b/DatadogRUM/Sources/Feature/RUMBaggageKeys.swift @@ -18,4 +18,8 @@ internal enum RUMBaggageKeys { /// The key references RUM session state. /// The state associated with the key conforms to `Codable`. static let sessionState = "rum-session-state" + + /// The key references ``DatadogInternal.GlobalRUMAttributes`` value holding RUM attributes. + /// It is sent after each change to RUM attributes in `RUMMonitor`. + static let attributes = "global-rum-attributes" } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index d6e4fd0680..09da91f3bb 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -43,9 +43,9 @@ internal final class RUMFeature: DatadogRemoteFeature { trackFrustrations: configuration.trackFrustrations, firstPartyHosts: { switch configuration.urlSessionTracking?.firstPartyHostsTracing { - case let .trace(hosts, _): + case let .trace(hosts, _, _): return FirstPartyHosts(hosts) - case let .traceWithHeaders(hostsWithHeaders, _): + case let .traceWithHeaders(hostsWithHeaders, _, _): return FirstPartyHosts(hostsWithHeaders) case .none: return nil @@ -55,6 +55,7 @@ internal final class RUMFeature: DatadogRemoteFeature { eventsMapper: eventsMapper ), rumUUIDGenerator: configuration.uuidGenerator, + backtraceReporter: core.backtraceReporter, ciTest: configuration.ciTestExecutionID.map { RUMCITest(testExecutionId: $0) }, syntheticsTest: { if let testId = configuration.syntheticsTestId, let resultId = configuration.syntheticsResultId { @@ -70,7 +71,8 @@ internal final class RUMFeature: DatadogRemoteFeature { ) }, onSessionStart: configuration.onSessionStart, - viewCache: ViewCache() + viewCache: ViewCache(), + fatalErrorContext: FatalErrorContextNotifier(messageBus: featureScope) ) self.monitor = Monitor( @@ -159,15 +161,15 @@ extension RUMFeature: Flushable { /// /// **blocks the caller thread** func flush() { - monitor.flush() + instrumentation.appHangs?.flush() } } private extension RUM.Configuration.URLSessionTracking.FirstPartyHostsTracing { var sampleRate: Float { switch self { - case .trace(_, let sampleRate): return sampleRate - case .traceWithHeaders(_, let sampleRate): return sampleRate + case .trace(_, let sampleRate, _): return sampleRate + case .traceWithHeaders(_, let sampleRate, _): return sampleRate } } } diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift index 4002c1a9a7..d9a69075ee 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift @@ -42,4 +42,6 @@ internal struct FatalAppHang: Codable { let lastRUMView: RUMViewEvent /// The user's consent at the moment of hang's recording. let trackingConsent: TrackingConsent + /// The App Launch date at the moment of hang's recording. + let appLaunchDate: Date? } diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift index d053d07e26..12c4b601af 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift @@ -31,7 +31,7 @@ internal final class AppHangsMonitor { appHangThreshold: TimeInterval, observedQueue: DispatchQueue, backtraceReporter: BacktraceReporting, - fatalErrorContext: FatalErrorContextNotifier, + fatalErrorContext: FatalErrorContextNotifying, dateProvider: DateProvider, processID: UUID ) { @@ -53,7 +53,7 @@ internal final class AppHangsMonitor { init( featureScope: FeatureScope, watchdogThread: AppHangsObservingThread, - fatalErrorContext: FatalErrorContextNotifier, + fatalErrorContext: FatalErrorContextNotifying, processID: UUID, dateProvider: DateProvider ) { diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift index ac5df9068c..fbe4152658 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift @@ -11,7 +11,7 @@ internal final class FatalAppHangsHandler { /// RUM feature scope. private let featureScope: FeatureScope /// RUM context for fatal App Hangs monitoring. - private let fatalErrorContext: FatalErrorContextNotifier + private let fatalErrorContext: FatalErrorContextNotifying /// An ID of the current process. private let processID: UUID /// Device date provider. @@ -19,7 +19,7 @@ internal final class FatalAppHangsHandler { init( featureScope: FeatureScope, - fatalErrorContext: FatalErrorContextNotifier, + fatalErrorContext: FatalErrorContextNotifying, processID: UUID, dateProvider: DateProvider ) { @@ -41,7 +41,8 @@ internal final class FatalAppHangsHandler { hang: hang, serverTimeOffset: context.serverTimeOffset, lastRUMView: lastRUMView, - trackingConsent: context.trackingConsent + trackingConsent: context.trackingConsent, + appLaunchDate: context.launchTime?.launchDate ) dataStore.setValue(fatalHang, forKey: .fatalAppHangKey) } @@ -92,6 +93,7 @@ internal final class FatalAppHangsHandler { let realErrorDate = fatalHang.hang.startDate.addingTimeInterval(fatalHang.serverTimeOffset) let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset) + let timeSinceAppStart = fatalHang.appLaunchDate.map { realErrorDate.timeIntervalSince($0) } let builder = FatalErrorBuilder( context: context, @@ -103,7 +105,8 @@ internal final class FatalAppHangsHandler { errorThreads: fatalHang.hang.backtraceResult.threads?.toRUMDataFormat, errorBinaryImages: fatalHang.hang.backtraceResult.binaryImages?.toRUMDataFormat, errorWasTruncated: fatalHang.hang.backtraceResult.wasTruncated, - errorMeta: nil + errorMeta: nil, + timeSinceAppStart: timeSinceAppStart ) let error = builder.createRUMError(with: fatalHang.lastRUMView) let view = builder.updateRUMViewWithError(fatalHang.lastRUMView) diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index e295fabf5f..13fc7b0bf4 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -47,7 +47,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { mainQueue: DispatchQueue, dateProvider: DateProvider, backtraceReporter: BacktraceReporting, - fatalErrorContext: FatalErrorContextNotifier, + fatalErrorContext: FatalErrorContextNotifying, processID: UUID ) { // Always create views handler (we can't know if it will be used by SwiftUI instrumentation) diff --git a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift index ab39b9c1cf..8c30d73983 100644 --- a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift @@ -15,17 +15,21 @@ internal struct DistributedTracing { let spanIDGenerator: SpanIDGenerator /// First party hosts defined by the user. let firstPartyHosts: FirstPartyHosts + /// Trace context injection configuration to determine whether the trace context should be injected or not. + let traceContextInjection: TraceContextInjection init( sampler: Sampler, firstPartyHosts: FirstPartyHosts, traceIDGenerator: TraceIDGenerator, - spanIDGenerator: SpanIDGenerator + spanIDGenerator: SpanIDGenerator, + traceContextInjection: TraceContextInjection ) { self.sampler = sampler self.traceIDGenerator = traceIDGenerator self.spanIDGenerator = spanIDGenerator self.firstPartyHosts = firstPartyHosts + self.traceContextInjection = traceContextInjection } } @@ -161,25 +165,31 @@ extension DistributedTracing { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(samplingStrategy: .headBased) + writer = HTTPHeadersWriter( + samplingStrategy: .headBased, + traceContextInjection: traceContextInjection + ) // 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( samplingStrategy: .headBased, - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: traceContextInjection ) case .b3multi: writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: traceContextInjection ) case .tracecontext: writer = W3CHTTPHeadersWriter( samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] + ], + traceContextInjection: traceContextInjection ) } diff --git a/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift b/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift index 80d0e1fc07..1af52bc244 100644 --- a/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift +++ b/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift @@ -23,6 +23,8 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { } struct CrashContext: Decodable { + /// The Application launch date + let appLaunchDate: Date? /// Interval between device and server time. let serverTimeOffset: TimeInterval /// The name of the service that data is generated from. @@ -41,6 +43,8 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { var lastRUMViewEvent: RUMViewEvent? /// State of the last RUM session in crashed app process. var lastRUMSessionState: RUMSessionState? + /// The last global RUM attributes in crashed app process. + var lastRUMAttributes: GlobalRUMAttributes? /// The last _"Is app in foreground?"_ information from crashed app process. let lastIsAppInForeground: Bool /// Network information. @@ -66,6 +70,8 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { let realCrashDate: Date /// Current time, adjusted with NTP correction. let realDateNow: Date + /// Time between crash and application launch + let timeSinceAppStart: TimeInterval? } /// RUM feature scope. @@ -127,14 +133,26 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { let currentTimeCorrection = context.serverTimeOffset let crashDate = report.date ?? dateProvider.now + var timeSinceAppStart: TimeInterval? = nil + if let startDate = context.appLaunchDate { + timeSinceAppStart = crashDate.timeIntervalSince(startDate) + } + let adjustedCrashTimings = AdjustedCrashTimings( crashDate: crashDate, realCrashDate: crashDate.addingTimeInterval(currentTimeCorrection), - realDateNow: dateProvider.now.addingTimeInterval(currentTimeCorrection) + realDateNow: dateProvider.now.addingTimeInterval(currentTimeCorrection), + timeSinceAppStart: timeSinceAppStart ) // RUMM-2516 if a cross-platform crash was reported, do not send its native version - if let lastRUMViewEvent = context.lastRUMViewEvent { + if var lastRUMViewEvent = context.lastRUMViewEvent { + if let lastRUMAttributes = context.lastRUMAttributes { + // RUM-3588: If last RUM attributes are available, use them to replace view attributes as we know that + // global RUM attributes can be updated more often than attributes in `lastRUMView`. + // See https://github.com/DataDog/dd-sdk-ios/pull/1834 for more context. + lastRUMViewEvent.context?.contextInfo = lastRUMAttributes.attributes + } if lastRUMViewEvent.view.crash?.count ?? 0 < 1 { sendCrashReportLinkedToLastViewInPreviousSession( report, @@ -165,13 +183,13 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { using crashTimings: AdjustedCrashTimings ) { if crashTimings.realDateNow.timeIntervalSince(crashTimings.realCrashDate) < FatalErrorBuilder.Constants.viewEventAvailabilityThreshold { - send(crashReport: crashReport, to: lastRUMViewEvent, using: crashTimings.realCrashDate) + send(crashReport: crashReport, to: lastRUMViewEvent, using: crashTimings) } else { // We know it is too late for sending RUM view to previous RUM session as it is now stale on backend. // To avoid inconsistency, we only send the RUM error. DD.logger.debug("Sending crash as RUM error.") featureScope.eventWriteContext(bypassConsent: true) { context, writer in - let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: crashTimings.realCrashDate) + let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: crashTimings.realCrashDate, timeSinceAppStart: crashTimings.timeSinceAppStart) let rumError = builder.createRUMError(with: lastRUMViewEvent) if let mappedError = self.eventsMapper.map(event: rumError) { @@ -231,7 +249,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { } if let newRUMView = newRUMView { - send(crashReport: crashReport, to: newRUMView, using: crashTimings.realCrashDate) + send(crashReport: crashReport, to: newRUMView, using: crashTimings) } } @@ -284,18 +302,18 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { } if let newRUMView = newRUMView { - send(crashReport: crashReport, to: newRUMView, using: crashTimings.realCrashDate) + send(crashReport: crashReport, to: newRUMView, using: crashTimings) } } /// Sends given `CrashReport` by linking it to given `rumView` and updating view counts accordingly. - private func send(crashReport: DDCrashReport, to rumView: RUMViewEvent, using realCrashDate: Date) { + private func send(crashReport: DDCrashReport, to rumView: RUMViewEvent, using crashTimings: AdjustedCrashTimings) { DD.logger.debug("Updating RUM view with crash report.") // crash reporting is considering the user consent from previous session, if an event reached // the message bus it means that consent was granted and we can safely bypass current consent. featureScope.eventWriteContext(bypassConsent: true) { context, writer in - let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: realCrashDate) + let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: crashTimings.realCrashDate, timeSinceAppStart: crashTimings.timeSinceAppStart) let updatedRUMView = builder.updateRUMViewWithError(rumView) let rumError = builder.createRUMError(with: updatedRUMView) @@ -313,7 +331,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { // MARK: - Building RUM events - private func createFatalErrorBuilder(context: DatadogContext, crash: DDCrashReport, crashDate: Date) -> FatalErrorBuilder { + private func createFatalErrorBuilder(context: DatadogContext, crash: DDCrashReport, crashDate: Date, timeSinceAppStart: TimeInterval?) -> FatalErrorBuilder { return FatalErrorBuilder( context: context, error: .crash, @@ -324,7 +342,8 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { errorThreads: crash.threads.toRUMDataFormat, errorBinaryImages: crash.binaryImages.toRUMDataFormat, errorWasTruncated: crash.wasTruncated, - errorMeta: crash.meta.toRUMDataFormat + errorMeta: crash.meta.toRUMDataFormat, + timeSinceAppStart: timeSinceAppStart ) } @@ -366,7 +385,10 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { carrierInfo: context.carrierInfo ), container: nil, - context: nil, + // RUM-3588: We know that last RUM view is not available, so we're creating a new one. No matter that, try using last + // RUM attributes if available. There is a chance of having them as global RUM attributes can be updated more often than RUM view. + // See https://github.com/DataDog/dd-sdk-ios/pull/1834 for more context. + context: context.lastRUMAttributes.map { .init(contextInfo: $0.attributes) }, date: startDate.timeIntervalSince1970.toInt64Milliseconds, device: .init(device: context.device, telemetry: featureScope.telemetry), display: nil, diff --git a/DatadogRUM/Sources/Integrations/ErrorMessageReceiver.swift b/DatadogRUM/Sources/Integrations/ErrorMessageReceiver.swift index 5f0f1c782e..31be16dbb6 100644 --- a/DatadogRUM/Sources/Integrations/ErrorMessageReceiver.swift +++ b/DatadogRUM/Sources/Integrations/ErrorMessageReceiver.swift @@ -12,7 +12,8 @@ internal struct ErrorMessageReceiver: FeatureMessageReceiver { struct ErrorMessage: Decodable { static let key = "error" - + /// The time of the log + let time: Date /// The Log error message let message: String /// The Log error kind @@ -23,6 +24,8 @@ internal struct ErrorMessageReceiver: FeatureMessageReceiver { let source: RUMInternalErrorSource /// The Log attributes let attributes: [String: AnyCodable]? + /// Binary images if need to decode the stack trace + let binaryImages: [BinaryImage]? } /// RUM feature scope. @@ -38,12 +41,14 @@ internal struct ErrorMessageReceiver: FeatureMessageReceiver { return false } - monitor.addError( + monitor._internal?.addError( + at: error.time, message: error.message, type: error.type, stack: error.stack, source: error.source, - attributes: error.attributes ?? [:] + attributes: error.attributes ?? [:], + binaryImages: error.binaryImages ) return true diff --git a/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift b/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift index 0e561e9073..1e90babbb3 100644 --- a/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift +++ b/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift @@ -137,7 +137,7 @@ internal final class TelemetryReceiver: FeatureMessageReceiver { /// - message: Body of the log /// - kind: The error type or kind (or code in some cases). /// - stack: The stack trace or the complementary information about the error. - private func error(id: String, message: String, kind: String?, stack: String?) { + private func error(id: String, message: String, kind: String, stack: String) { let date = dateProvider.now record(event: id) { context, writer in @@ -290,6 +290,7 @@ private extension TelemetryConfigurationEvent.Telemetry.Configuration { batchProcessingLevel: configuration.batchProcessingLevel, batchSize: configuration.batchSize, batchUploadFrequency: configuration.batchUploadFrequency, + compressIntakeRequests: nil, dartVersion: configuration.dartVersion, defaultPrivacyLevel: nil, forwardConsoleLogs: nil, @@ -308,7 +309,10 @@ private extension TelemetryConfigurationEvent.Telemetry.Configuration { storeContextsAcrossPages: nil, telemetryConfigurationSampleRate: nil, telemetrySampleRate: configuration.telemetrySampleRate, + telemetryUsageSampleRate: nil, traceSampleRate: configuration.traceSampleRate, + tracerApi: configuration.tracerAPI, + tracerApiVersion: configuration.tracerAPIVersion, trackBackgroundEvents: configuration.trackBackgroundEvents, trackCrossPlatformLongTasks: configuration.trackCrossPlatformLongTasks, trackErrors: configuration.trackErrors, @@ -326,6 +330,7 @@ private extension TelemetryConfigurationEvent.Telemetry.Configuration { trackSessionAcrossSubdomains: nil, trackUserInteractions: configuration.trackUserInteractions, trackViewsManually: configuration.trackViewsManually, + trackingConsent: nil, unityVersion: configuration.unityVersion, useAllowedTracingOrigins: nil, useAllowedTracingUrls: nil, diff --git a/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift b/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift index a578ce340f..22e61805dd 100644 --- a/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift +++ b/DatadogRUM/Sources/Integrations/WebViewEventReceiver.swift @@ -76,11 +76,9 @@ internal final class WebViewEventReceiver: FeatureMessageReceiver { let rum: RUMCoreContext = try rumBaggage.decode() var event = event - if - let date = event["date"] as? Int, - let view = event["view"] as? JSON, - let id = view["id"] as? String - { + 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 diff --git a/DatadogRUM/Sources/RUM+Internal.swift b/DatadogRUM/Sources/RUM+Internal.swift index 19c6286454..6c88d1f00a 100644 --- a/DatadogRUM/Sources/RUM+Internal.swift +++ b/DatadogRUM/Sources/RUM+Internal.swift @@ -49,19 +49,21 @@ extension InternalExtension where ExtendedType == RUM { // If first party hosts are configured, enable distributed tracing: switch configuration.firstPartyHostsTracing { - case let .trace(hosts, sampleRate): + case let .trace(hosts, sampleRate, traceContextInjection): distributedTracing = DistributedTracing( sampler: Sampler(samplingRate: rumConfiguration.debugSDK ? 100 : sampleRate), firstPartyHosts: FirstPartyHosts(hosts), traceIDGenerator: rumConfiguration.traceIDGenerator, - spanIDGenerator: rumConfiguration.spanIDGenerator + spanIDGenerator: rumConfiguration.spanIDGenerator, + traceContextInjection: traceContextInjection ) - case let .traceWithHeaders(hostsWithHeaders, sampleRate): + case let .traceWithHeaders(hostsWithHeaders, sampleRate, traceContextInjection): distributedTracing = DistributedTracing( sampler: Sampler(samplingRate: rumConfiguration.debugSDK ? 100 : sampleRate), firstPartyHosts: FirstPartyHosts(hostsWithHeaders), traceIDGenerator: rumConfiguration.traceIDGenerator, - spanIDGenerator: rumConfiguration.spanIDGenerator + spanIDGenerator: rumConfiguration.spanIDGenerator, + traceContextInjection: traceContextInjection ) case .none: distributedTracing = nil diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index bc8d6a05d2..ffff00b710 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -13,6 +13,7 @@ import DatadogInternal @_exported import typealias DatadogInternal.DDURLSessionDelegate @_exported import protocol DatadogInternal.__URLSessionDelegateProviding @_exported import enum DatadogInternal.URLSessionInstrumentation +@_exported import enum DatadogInternal.TraceContextInjection // swiftlint:enable duplicate_imports extension RUM { @@ -294,13 +295,23 @@ extension RUM.Configuration.URLSessionTracking { /// - Parameters: /// - hosts: The set of hosts to inject tracing headers. Note: Hosts must not include the "http(s)://" prefix. /// - sampleRate: The sampling rate for tracing. Must be a value between `0.0` and `100.0`. Default: `20`. - case trace(hosts: Set, sampleRate: Float = 20) + /// - traceControlInjection: The strategy for injecting trace context into requests. Default: `.all`. + case trace( + hosts: Set, + sampleRate: Float = 20, + traceControlInjection: TraceContextInjection = .all + ) /// Trace given hosts with using custom tracing headers. /// /// - `hostsWithHeaders` - Dictionary of hosts and tracing header types to use. Note: Hosts must not include "http(s)://" prefix. /// - `sampleRate` - The sampling rate for tracing. Must be a value between `0.0` and `100.0`. Default: `20`. - case traceWithHeaders(hostsWithHeaders: [String: Set], sampleRate: Float = 20) + /// - `traceControlInjection` - The strategy for injecting trace context into requests. Default: `.all`. + case traceWithHeaders( + hostsWithHeaders: [String: Set], + sampleRate: Float = 20, + traceControlInjection: TraceContextInjection = .all + ) } /// Configuration for automatic RUM resources tracking. diff --git a/DatadogRUM/Sources/RUMMonitor/Monitor.swift b/DatadogRUM/Sources/RUMMonitor/Monitor.swift index 049f74b273..4621b81e45 100644 --- a/DatadogRUM/Sources/RUMMonitor/Monitor.swift +++ b/DatadogRUM/Sources/RUMMonitor/Monitor.swift @@ -114,14 +114,18 @@ internal class Monitor: RUMCommandSubscriber { let featureScope: FeatureScope let scopes: RUMApplicationScope let dateProvider: DateProvider - let queue = DispatchQueue( - label: "com.datadoghq.rum-monitor", - target: .global(qos: .userInteractive) - ) + @ReadWriteLock private(set) var debugging: RUMDebugging? = nil - private var attributes: [AttributeKey: AttributeValue] = [:] + @ReadWriteLock + private var attributes: [AttributeKey: AttributeValue] = [:] { + didSet { + fatalErrorContext.globalAttributes = attributes + } + } + + private let fatalErrorContext: FatalErrorContextNotifying init( dependencies: RUMScopeDependencies, @@ -130,43 +134,48 @@ internal class Monitor: RUMCommandSubscriber { self.featureScope = dependencies.featureScope self.scopes = RUMApplicationScope(dependencies: dependencies) self.dateProvider = dateProvider + self.fatalErrorContext = dependencies.fatalErrorContext } func process(command: RUMCommand) { // process command in event context - featureScope.eventWriteContext { context, writer in - self.queue.sync { - let transformedCommand = self.transform(command: command) + featureScope.eventWriteContext { [weak self] context, writer in + guard let self = self else { + return + } - _ = self.scopes.process(command: transformedCommand, context: context, writer: writer) + let transformedCommand = self.transform(command: command) - if let debugging = self.debugging { - debugging.debug(applicationScope: self.scopes) - } + _ = self.scopes.process(command: transformedCommand, context: context, writer: writer) + + if let debugging = self.debugging { + debugging.debug(applicationScope: self.scopes) } } // update the core context with rum context featureScope.set( - baggage: { - self.queue.sync { () -> RUMCoreContext? in - let context = self.scopes.activeSession?.viewScopes.last?.context ?? - self.scopes.activeSession?.context ?? - self.scopes.context - - guard context.sessionID != .nullUUID else { - // if Session was sampled or not yet started - return nil - } - - return RUMCoreContext( - applicationID: context.rumApplicationID, - sessionID: context.sessionID.rawValue.uuidString.lowercased(), - viewID: context.activeViewID?.rawValue.uuidString.lowercased(), - userActionID: context.activeUserActionID?.rawValue.uuidString.lowercased(), - viewServerTimeOffset: self.scopes.activeSession?.viewScopes.last?.serverTimeOffset - ) + baggage: { [weak self] () -> RUMCoreContext? in + guard let self = self else { + return nil } + + let context = self.scopes.activeSession?.viewScopes.last?.context ?? + self.scopes.activeSession?.context ?? + self.scopes.context + + guard context.sessionID != .nullUUID else { + // if Session was sampled or not yet started + return nil + } + + return RUMCoreContext( + applicationID: context.rumApplicationID, + sessionID: context.sessionID.rawValue.uuidString.lowercased(), + viewID: context.activeViewID?.rawValue.uuidString.lowercased(), + userActionID: context.activeUserActionID?.rawValue.uuidString.lowercased(), + viewServerTimeOffset: self.scopes.activeSession?.viewScopes.last?.serverTimeOffset + ) }, forKey: RUMFeature.name ) @@ -201,37 +210,30 @@ extension Monitor: RUMMonitorProtocol { // MARK: - attributes func addAttribute(forKey key: AttributeKey, value: AttributeValue) { - queue.async { - self.attributes[key] = value - } + attributes[key] = value } func removeAttribute(forKey key: AttributeKey) { - queue.async { - self.attributes[key] = nil - } + attributes[key] = nil } // MARK: - session func currentSessionID(completion: @escaping (String?) -> Void) { - // Even though we're not writing anything, need to get the write context - // to make sure we're returning the correct sessionId after all other - // events have processed. - featureScope.eventWriteContext { _, _ in - self.queue.sync { - guard let sessionId = self.scopes.activeSession?.sessionUUID else { - completion(nil) - return - } - - var sessionIdValue: String? = nil - if sessionId != RUMUUID.nullUUID { - sessionIdValue = sessionId.rawValue.uuidString - } + // Synchronise it through the context thread to make sure we return the correct + // sessionID after all other events have been processed (also on the context thread): + featureScope.context { [weak self] _ in + guard let sessionId = self?.scopes.activeSession?.sessionUUID else { + completion(nil) + return + } - completion(sessionIdValue) + var sessionIdValue: String? = nil + if sessionId != RUMUUID.nullUUID { + sessionIdValue = sessionId.rawValue.uuidString } + + completion(sessionIdValue) } } @@ -500,15 +502,19 @@ extension Monitor: RUMMonitorProtocol { var debug: Bool { set { - queue.async { - self.debugging = newValue ? RUMDebugging() : nil + debugging = newValue ? RUMDebugging() : nil + + // Synchronise `debug(applicationScope:)` through the context thread to make sure it can safely + // read `scopes` after all events have been processed (also on the context thread): + featureScope.context { [weak self] _ in + guard let self = self else { + return + } self.debugging?.debug(applicationScope: self.scopes) } } get { - queue.sync { - self.debugging != nil - } + debugging != nil } } } @@ -540,9 +546,4 @@ extension Monitor { ) ) } - - /// Completes all asynchronous operations with blocking the caller thread. - func flush() { - queue.sync { } - } } diff --git a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift index ddee818abc..8c29297761 100644 --- a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift +++ b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift @@ -14,7 +14,7 @@ internal protocol RUMCommand { /// Attributes associated with the command. var attributes: [AttributeKey: AttributeValue] { set get } /// Whether or not receiving this command should start the "Background" view if no view is active - /// and ``Datadog.Configuration.Builder.trackBackgroundEvents(_:)`` is enabled. + /// and ``RUM.Configuration.trackBackgroundEvents`` is enabled. var canStartBackgroundView: Bool { get } /// Whether or not this command is considered a user intaraction var isUserInteraction: Bool { get } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/FatalErrorContextNotifier.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/FatalErrorContextNotifier.swift index b14132ba39..54fd14bef6 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/FatalErrorContextNotifier.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/FatalErrorContextNotifier.swift @@ -7,9 +7,22 @@ import Foundation import DatadogInternal -/// Manages RUM information necessary for building context for fatal errors such as Crashes or Fatal App Hangs. +/// A type managing RUM information necessary for building context of fatal errors such as Crashes or Fatal App Hangs. +internal protocol FatalErrorContextNotifying: AnyObject { + /// The state of the current RUM session. + /// Can be `nil` if no session was yet started. Never gets `nil` after starting first session. + var sessionState: RUMSessionState? { set get } + /// The active RUM view in current session. + /// Can be `nil` if no view is yet started. Will become `nil` if view was stopped without starting the new one. + var view: RUMViewEvent? { set get } + /// The current set of RUM attributes. + /// It gets updated with global attributes being added or removed in `RUMMonitor`. + var globalAttributes: [String: Encodable] { set get } +} + +/// Manages RUM information necessary for building context of fatal errors such as Crashes or Fatal App Hangs. /// It tracks value changes and notifies updates on message bus. -internal final class FatalErrorContextNotifier { +internal final class FatalErrorContextNotifier: FatalErrorContextNotifying { /// Message bus interface to send context updates to Crash Reporting. private let messageBus: MessageSending @@ -40,4 +53,16 @@ internal final class FatalErrorContextNotifier { } } } + + @ReadWriteLock + var globalAttributes: [String: Encodable] = [:] { + didSet { + messageBus.send( + message: .baggage( + key: RUMBaggageKeys.attributes, + value: GlobalRUMAttributes(attributes: globalAttributes) + ) + ) + } + } } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift index a9b5f32074..65e6dab356 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -46,7 +46,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // Notify Synthetics if needed if dependencies.syntheticsTest != nil { - print("_dd.application.id=" + dependencies.rumApplicationID) + NSLog("_dd.application.id=" + dependencies.rumApplicationID) } } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift index 9e91eb2ad5..41cd687c0d 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift @@ -198,6 +198,7 @@ internal class RUMResourceScope: RUMScope { start: metric.start.timeIntervalSince(resourceStartTime).toInt64Nanoseconds ) }, + decodedBodySize: nil, dns: resourceMetrics?.dns.map { metric in .init( duration: metric.duration.toInt64Nanoseconds, @@ -211,6 +212,7 @@ internal class RUMResourceScope: RUMScope { ) }, duration: resolveResourceDuration(resourceDuration), + encodedBodySize: nil, firstByte: resourceMetrics?.firstByte.map { metric in .init( duration: metric.duration.toInt64Nanoseconds, @@ -227,6 +229,7 @@ internal class RUMResourceScope: RUMScope { start: metric.start.timeIntervalSince(resourceStartTime).toInt64Nanoseconds ) }, + renderBlockingStatus: nil, size: size ?? 0, ssl: resourceMetrics?.ssl.map { metric in .init( @@ -235,6 +238,7 @@ internal class RUMResourceScope: RUMScope { ) }, statusCode: command.httpStatusCode?.toInt64 ?? 0, + transferSize: nil, type: resourceType, url: resourceURL ), @@ -266,6 +270,10 @@ internal class RUMResourceScope: RUMScope { attributes.merge(rumCommandAttributes: command.attributes) let errorFingerprint = attributes.removeValue(forKey: RUM.Attributes.errorFingerprint) as? String + var timeSinceAppStart: Int64? = nil + if let startTime = context.launchTime?.launchDate { + timeSinceAppStart = command.time.timeIntervalSince(startTime).toInt64Milliseconds + } let errorEvent = RUMErrorEvent( dd: .init( @@ -292,6 +300,7 @@ internal class RUMResourceScope: RUMScope { error: .init( binaryImages: nil, category: .exception, // resource errors are categorised as "Exception" + csp: nil, fingerprint: errorFingerprint, handling: nil, handlingStack: nil, @@ -309,6 +318,7 @@ internal class RUMResourceScope: RUMScope { sourceType: command.errorSourceType, stack: command.stack, threads: nil, + timeSinceAppStart: timeSinceAppStart, type: command.errorType, wasTruncated: nil ), diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift index f0258a1486..d325ac39ec 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift @@ -36,6 +36,7 @@ internal struct RUMScopeDependencies { let firstPartyHosts: FirstPartyHosts? let eventBuilder: RUMEventBuilder let rumUUIDGenerator: RUMUUIDGenerator + let backtraceReporter: BacktraceReporting? /// Integration with CIApp tests. It contains the CIApp test context when active. let ciTest: RUMCITest? let syntheticsTest: RUMSyntheticsTest? @@ -43,7 +44,7 @@ internal struct RUMScopeDependencies { let onSessionStart: RUM.SessionListener? let viewCache: ViewCache /// The RUM context necessary for tracking fatal errors like Crashes or fatal App Hangs. - let fatalErrorContext: FatalErrorContextNotifier + let fatalErrorContext: FatalErrorContextNotifying /// Telemetry endpoint. let telemetry: Telemetry let sessionType: RUMSessionType @@ -57,11 +58,13 @@ internal struct RUMScopeDependencies { firstPartyHosts: FirstPartyHosts?, eventBuilder: RUMEventBuilder, rumUUIDGenerator: RUMUUIDGenerator, + backtraceReporter: BacktraceReporting?, ciTest: RUMCITest?, syntheticsTest: RUMSyntheticsTest?, vitalsReaders: VitalsReaders?, onSessionStart: RUM.SessionListener?, - viewCache: ViewCache + viewCache: ViewCache, + fatalErrorContext: FatalErrorContextNotifying ) { self.featureScope = featureScope self.rumApplicationID = rumApplicationID @@ -71,12 +74,13 @@ internal struct RUMScopeDependencies { self.firstPartyHosts = firstPartyHosts self.eventBuilder = eventBuilder self.rumUUIDGenerator = rumUUIDGenerator + self.backtraceReporter = backtraceReporter self.ciTest = ciTest self.syntheticsTest = syntheticsTest self.vitalsReaders = vitalsReaders self.onSessionStart = onSessionStart self.viewCache = viewCache - self.fatalErrorContext = FatalErrorContextNotifier(messageBus: featureScope) + self.fatalErrorContext = fatalErrorContext self.telemetry = featureScope.telemetry if ciTest != nil { diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index a3d9041e78..5154f12e38 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -122,7 +122,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // Notify Synthetics if needed if dependencies.syntheticsTest != nil && sessionUUID != .nullUUID { - print("_dd.session.id=" + sessionUUID.toRUMDataFormat) + NSLog("_dd.session.id=" + sessionUUID.toRUMDataFormat) } } @@ -283,11 +283,6 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { timestamp: startTime.timeIntervalSince1970.toInt64Milliseconds, hasReplay: hasReplay ) - - // Notify Synthetics if needed - if dependencies.syntheticsTest != nil && sessionUUID != .nullUUID { - print("_dd.view.id=" + id) - } } private func startApplicationLaunchView(on command: RUMApplicationStartCommand, context: DatadogContext, writer: Writer) { diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index 1a98a5e1d2..031fa23332 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -124,6 +124,11 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { frequency: $0.frequency ) } + + // Notify Synthetics if needed + if dependencies.syntheticsTest != nil && self.context.sessionID != .nullUUID { + NSLog("_dd.view.id=" + self.viewUUID.toRUMDataFormat) + } } // MARK: - RUMContextProvider @@ -552,6 +557,19 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { var commandAttributes = command.attributes let errorFingerprint = commandAttributes.removeValue(forKey: RUM.Attributes.errorFingerprint) as? String + var timeSinceAppStart: Int64? = nil + if let startTime = context.launchTime?.launchDate { + timeSinceAppStart = command.time.timeIntervalSince(startTime).toInt64Milliseconds + } + + var binaryImages = command.binaryImages?.compactMap { $0.toRUMDataFormat } + if commandAttributes.removeValue(forKey: CrossPlatformAttributes.includeBinaryImages) != nil { + // Don't try to get binary images if we already have them. + if binaryImages == nil { + // TODO: RUM-4072 Replace full backtrace reporter with simpler binary image fetcher + binaryImages = try? dependencies.backtraceReporter?.generateBacktrace()?.binaryImages.compactMap { $0.toRUMDataFormat } + } + } let errorEvent = RUMErrorEvent( dd: .init( @@ -576,9 +594,10 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { device: .init(context: context, telemetry: dependencies.telemetry), display: nil, error: .init( - binaryImages: command.binaryImages?.compactMap { $0.toRUMDataFormat }, + binaryImages: binaryImages, category: command.category, causes: nil, + csp: nil, fingerprint: errorFingerprint, handling: nil, handlingStack: nil, @@ -591,6 +610,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { sourceType: command.errorSourceType, stack: command.stack, threads: command.threads?.compactMap { $0.toRUMDataFormat }, + timeSinceAppStart: timeSinceAppStart, type: command.type, wasTruncated: command.isStackTraceTruncated ), diff --git a/DatadogRUM/Sources/RUMMonitorProtocol+Internal.swift b/DatadogRUM/Sources/RUMMonitorProtocol+Internal.swift index f7222b93fa..a447f38014 100644 --- a/DatadogRUM/Sources/RUMMonitorProtocol+Internal.swift +++ b/DatadogRUM/Sources/RUMMonitorProtocol+Internal.swift @@ -27,6 +27,32 @@ public extension RUMMonitorProtocol { public struct DatadogInternalInterface { let monitor: RUMCommandSubscriber + /// Adds a RUM error to the current view, allowing the addition of BinaryImages + /// which can be used to symbolicate stack traces that are not provided by PLCrashReporter + internal func addError( + at time: Date, + message: String, + type: String?, + stack: String?, + source: RUMInternalErrorSource, + attributes: [AttributeKey: AttributeValue], + binaryImages: [BinaryImage]? + ) { + let addErrorCommand = RUMAddCurrentViewErrorCommand( + time: time, + message: message, + type: type, + stack: stack, + source: source, + isCrash: false, + threads: nil, + binaryImages: binaryImages, + isStackTraceTruncated: nil, + attributes: attributes + ) + monitor.process(command: addErrorCommand) + } + /// Adds RUM long task to current view. /// - Parameters: /// - time: the time of this command in cross-platform SDK diff --git a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift index 0aabdba81c..9cac5af72d 100644 --- a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift +++ b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift @@ -414,4 +414,38 @@ class AppHangsMonitorTests: XCTestCase { DDAssertJSONEqual(errorEvent.error.binaryImages, hangBacktrace.binaryImages.toRUMDataFormat) XCTAssertEqual(errorEvent.error.wasTruncated, hangBacktrace.wasTruncated) } + + func testWhenSendingRUMErrorEvent_itIncludesTimeSinceAppLaunch() throws { + let appLaunchDate: Date = .mockDecember15th2019At10AMUTC() + let hangTimeSinceAppStart: TimeInterval = .mockRandom(min: 1, max: 10) + let hang: AppHang = .mockWith(startDate: appLaunchDate.addingTimeInterval(hangTimeSinceAppStart)) + + // Given (track hang in previous app session) + featureScope.contextMock.trackingConsent = .granted + featureScope.contextMock.launchTime = .mockWith(launchDate: appLaunchDate) + featureScope.contextMock.serverTimeOffset = 0 + monitor.start() + fatalErrorContext.view = .mockRandom() + watchdogThread.delegate?.hangStarted(hang) + monitor.stop() + + // When (app is restarted) + let appRestartDate = appLaunchDate.addingTimeInterval(.mockRandom(min: 10, max: 100)) + featureScope.contextMock.launchTime = .mockWith(launchDate: appRestartDate) + featureScope.contextMock.serverTimeOffset = .mockRandom(min: 0, max: 100) + let monitor = AppHangsMonitor( + featureScope: featureScope, + watchdogThread: watchdogThread, + fatalErrorContext: fatalErrorContext, + processID: UUID(), // different process + dateProvider: DateProviderMock(now: appRestartDate) + ) + monitor.start() + defer { monitor.stop() } + + // Then + let errorEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self).first) + XCTAssertEqual(errorEvent.error.category, .appHang) + XCTAssertEqual(errorEvent.error.timeSinceAppStart, hangTimeSinceAppStart.toInt64Milliseconds) + } } diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 476587d873..9cb84d1fb9 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -23,7 +23,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) @@ -48,7 +48,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) @@ -70,7 +70,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) @@ -95,7 +95,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) @@ -116,7 +116,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) @@ -137,7 +137,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) @@ -158,7 +158,7 @@ class RUMInstrumentationTests: XCTestCase { mainQueue: .main, dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), - fatalErrorContext: .mockAny(), + fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny() ) let subscriber = RUMCommandSubscriberMock() diff --git a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift index d4d5a7c5aa..b8ffaad2db 100644 --- a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift +++ b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift @@ -35,7 +35,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockKeepAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -66,7 +67,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockKeepAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -94,7 +96,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockKeepAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -125,7 +128,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockKeepAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -153,7 +157,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockRejectAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .sampled ) ) @@ -166,7 +171,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField), "rum") XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField)) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField)) - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField)) XCTAssertNil(traceContext, "It must return no trace context") } @@ -178,7 +183,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockRejectAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -201,7 +207,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockRejectAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -227,7 +234,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockRejectAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -250,7 +258,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockKeepAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) @@ -332,7 +341,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: Sampler(samplingRate: Float(traceSamplingRate)), firstPartyHosts: .init(), traceIDGenerator: DefaultTraceIDGenerator(), - spanIDGenerator: DefaultSpanIDGenerator() + spanIDGenerator: DefaultSpanIDGenerator(), + traceContextInjection: .all ) ) @@ -515,7 +525,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { sampler: .mockKeepAll(), firstPartyHosts: .init(), traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), - spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0) + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + traceContextInjection: .all ) ) let request: URLRequest = .mockWith(httpMethod: "GET") diff --git a/DatadogRUM/Tests/Integrations/ErrorMessageReceiverTests.swift b/DatadogRUM/Tests/Integrations/ErrorMessageReceiverTests.swift index 81bb407504..92d12127f6 100644 --- a/DatadogRUM/Tests/Integrations/ErrorMessageReceiverTests.swift +++ b/DatadogRUM/Tests/Integrations/ErrorMessageReceiverTests.swift @@ -44,12 +44,14 @@ class ErrorMessageReceiverTests: XCTestCase { func testReceivePartialError() throws { // When + let baggage: [String: Any] = [ + "time": Date(), + "message": "message-test", + "source": "custom" + ] let message: FeatureMessage = .baggage( key: ErrorMessageReceiver.ErrorMessage.key, - value: [ - "message": "message-test", - "source": "custom" - ] + value: AnyEncodable(baggage) ) let result = receiver.receive(message: message, from: NOPDatadogCore()) @@ -62,11 +64,16 @@ class ErrorMessageReceiverTests: XCTestCase { func testReceiveCompleteError() throws { let mockAttribute: String = .mockRandom() + let mockBinaryImage: BinaryImage = .mockRandom() let baggage: [String: Any] = [ + "time": Date(), "message": "message-test", "type": "type-test", "stack": "stack-test", "source": "logger", + "binaryImages": [ + mockBinaryImage + ], "attributes": [ "any-key": mockAttribute ] @@ -83,6 +90,17 @@ class ErrorMessageReceiverTests: XCTestCase { XCTAssertEqual(event.error.type, "type-test") XCTAssertEqual(event.error.stack, "stack-test") XCTAssertEqual(event.error.source, .logger) + XCTAssertNotNil(event.error.binaryImages) + XCTAssertEqual(event.error.binaryImages?.count, 1) + if let image = event.error.binaryImages?.first { + XCTAssertEqual(mockBinaryImage.libraryName, image.name) + XCTAssertEqual(mockBinaryImage.uuid, image.uuid) + XCTAssertEqual(mockBinaryImage.architecture, image.arch) + XCTAssertEqual(mockBinaryImage.isSystemLibrary, image.isSystem) + XCTAssertEqual(mockBinaryImage.loadAddress, image.loadAddress) + XCTAssertEqual(mockBinaryImage.maxAddress, image.maxAddress) + } + let attributeValue = (event.context?.contextInfo["any-key"] as? AnyCodable)?.value as? String XCTAssertEqual(attributeValue, mockAttribute) } diff --git a/DatadogRUM/Tests/Integrations/TelemetryReceiverTests.swift b/DatadogRUM/Tests/Integrations/TelemetryReceiverTests.swift index afabbe8f3a..8dfb2a28ed 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryReceiverTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryReceiverTests.swift @@ -129,7 +129,7 @@ class TelemetryReceiverTests: XCTestCase { // When telemetry.debug(id: "0", message: "telemetry debug 0") - telemetry.error(id: "0", message: "telemetry debug 1", kind: nil, stack: nil) + telemetry.error(id: "0", message: "telemetry debug 1", kind: "error.kind", stack: "error.stack") telemetry.debug(id: "0", message: "telemetry debug 2") telemetry.debug(id: "1", message: "telemetry debug 3") @@ -443,7 +443,7 @@ class TelemetryReceiverTests: XCTestCase { // Then let event = featureScope.eventsWritten(ofType: TelemetryDebugEvent.self).first XCTAssertEqual(event?.telemetry.message, "[Mobile Metric] Method Called") - XCTAssertEqual(try XCTUnwrap(event?.telemetry.telemetryInfo[BasicMetric.typeKey] as? String), MethodCalledMetric.typeValue) + XCTAssertEqual(try XCTUnwrap(event?.telemetry.telemetryInfo[SDKMetricFields.typeKey] as? String), MethodCalledMetric.typeValue) XCTAssertEqual(try XCTUnwrap(event?.telemetry.telemetryInfo[MethodCalledMetric.operationName] as? String), operationName) XCTAssertEqual(try XCTUnwrap(event?.telemetry.telemetryInfo[MethodCalledMetric.callerClass] as? String), callerClass) XCTAssertEqual(try XCTUnwrap(event?.telemetry.telemetryInfo[MethodCalledMetric.isSuccessful] as? Bool), isSuccessful) diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index b6020bc788..ab08fe5a1b 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -259,9 +259,11 @@ extension RUMResourceEvent: RandomMockable { os: .mockRandom(), resource: .init( connect: .init(duration: .mockRandom(), start: .mockRandom()), + decodedBodySize: nil, dns: .init(duration: .mockRandom(), start: .mockRandom()), download: .init(duration: .mockRandom(), start: .mockRandom()), duration: .mockRandom(), + encodedBodySize: nil, firstByte: .init(duration: .mockRandom(), start: .mockRandom()), id: .mockRandom(), method: .mockRandom(), @@ -271,9 +273,11 @@ extension RUMResourceEvent: RandomMockable { type: Bool.random() ? .firstParty : nil ), redirect: .init(duration: .mockRandom(), start: .mockRandom()), + renderBlockingStatus: nil, size: .mockRandom(), ssl: .init(duration: .mockRandom(), start: .mockRandom()), statusCode: .mockRandom(), + transferSize: nil, type: [.native, .image].randomElement()!, url: .mockRandom() ), @@ -400,6 +404,7 @@ extension RUMErrorEvent: RandomMockable { error: .init( binaryImages: nil, category: nil, + csp: nil, handling: nil, handlingStack: nil, id: .mockRandom(), @@ -420,6 +425,7 @@ extension RUMErrorEvent: RandomMockable { sourceType: .mockRandom(), stack: .mockRandom(), threads: nil, + timeSinceAppStart: nil, type: .mockRandom(), wasTruncated: .mockRandom() ), @@ -512,6 +518,7 @@ extension TelemetryConfigurationEvent: RandomMockable { batchProcessingLevel: .mockRandom(), batchSize: .mockAny(), batchUploadFrequency: .mockRandom(), + compressIntakeRequests: nil, defaultPrivacyLevel: .mockRandom(), forwardConsoleLogs: nil, forwardErrorsToLogs: nil, @@ -529,6 +536,7 @@ extension TelemetryConfigurationEvent: RandomMockable { storeContextsAcrossPages: nil, telemetryConfigurationSampleRate: .mockRandom(), telemetrySampleRate: .mockRandom(), + telemetryUsageSampleRate: nil, traceSampleRate: .mockRandom(), trackBackgroundEvents: .mockRandom(), trackCrossPlatformLongTasks: .mockRandom(), @@ -544,6 +552,7 @@ extension TelemetryConfigurationEvent: RandomMockable { trackResources: .mockRandom(), trackSessionAcrossSubdomains: nil, trackViewsManually: nil, + trackingConsent: nil, useAllowedTracingOrigins: .mockRandom(), useAllowedTracingUrls: nil, useBeforeSend: nil, diff --git a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift index 6ebaef88d3..7266735fe3 100644 --- a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift @@ -745,12 +745,10 @@ func mockNoOpSessionListener() -> RUM.SessionListener { return { _, _ in } } -extension FatalErrorContextNotifier: AnyMockable { - public static func mockAny() -> FatalErrorContextNotifier { - return FatalErrorContextNotifier( - messageBus: NOPFeatureScope() - ) - } +internal class FatalErrorContextNotifierMock: FatalErrorContextNotifying { + var sessionState: RUMSessionState? + var view: RUMViewEvent? + var globalAttributes: [String: Encodable] = [:] } extension RUMScopeDependencies { @@ -767,11 +765,13 @@ extension RUMScopeDependencies { firstPartyHosts: FirstPartyHosts = .init([:]), eventBuilder: RUMEventBuilder = RUMEventBuilder(eventsMapper: .mockNoOp()), rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), + backtraceReporter: BacktraceReporting = BacktraceReporterMock(backtrace: nil), ciTest: RUMCITest? = nil, syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache() + viewCache: ViewCache = ViewCache(), + fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock() ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: featureScope, @@ -782,11 +782,13 @@ extension RUMScopeDependencies { firstPartyHosts: firstPartyHosts, eventBuilder: eventBuilder, rumUUIDGenerator: rumUUIDGenerator, + backtraceReporter: backtraceReporter, ciTest: ciTest, syntheticsTest: syntheticsTest, vitalsReaders: vitalsReaders, onSessionStart: onSessionStart, - viewCache: viewCache + viewCache: viewCache, + fatalErrorContext: fatalErrorContext ) } @@ -799,11 +801,13 @@ extension RUMScopeDependencies { firstPartyHosts: FirstPartyHosts? = nil, eventBuilder: RUMEventBuilder? = nil, rumUUIDGenerator: RUMUUIDGenerator? = nil, + backtraceReporter: BacktraceReporting? = nil, ciTest: RUMCITest? = nil, syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: RUM.SessionListener? = nil, - viewCache: ViewCache? = nil + viewCache: ViewCache? = nil, + fatalErrorContext: FatalErrorContextNotifying? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: self.featureScope, @@ -814,11 +818,13 @@ extension RUMScopeDependencies { firstPartyHosts: firstPartyHosts ?? self.firstPartyHosts, eventBuilder: eventBuilder ?? self.eventBuilder, rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator, + backtraceReporter: backtraceReporter ?? self.backtraceReporter, ciTest: ciTest ?? self.ciTest, syntheticsTest: syntheticsTest ?? self.syntheticsTest, vitalsReaders: vitalsReaders ?? self.vitalsReaders, onSessionStart: onSessionStart ?? self.onSessionStart, - viewCache: viewCache ?? self.viewCache + viewCache: viewCache ?? self.viewCache, + fatalErrorContext: fatalErrorContext ?? self.fatalErrorContext ) } } diff --git a/DatadogRUM/Tests/RUMMonitor/Monitor+GlobalAttributesTests.swift b/DatadogRUM/Tests/RUMMonitor/Monitor+GlobalAttributesTests.swift new file mode 100644 index 0000000000..207e85d524 --- /dev/null +++ b/DatadogRUM/Tests/RUMMonitor/Monitor+GlobalAttributesTests.swift @@ -0,0 +1,542 @@ +/* + * 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 DatadogRUM + +class Monitor_GlobalAttributesTests: XCTestCase { + private let featureScope = FeatureScopeMock() + private var monitor: Monitor! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + monitor = Monitor( + dependencies: .mockWith(featureScope: featureScope), + dateProvider: SystemDateProvider() + ) + } + + override func tearDown() { + monitor = nil + } + + // MARK: - Changing Global Attributes After SDK Init + + func testAddingGlobalAttributeAfterSDKInit() throws { + // When + monitor.notifySDKInit() + monitor.addAttribute(forKey: "attribute", value: "value") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName) + XCTAssertNil(lastView.attribute(forKey: "attribute")) + } + + func testAddingGlobalAttributeAfterSDKInit_thenStartingView() throws { + // Given + monitor.notifySDKInit() + monitor.addAttribute(forKey: "attribute", value: "value") + + // When + monitor.startView(key: "View") + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let appLaunchView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName })) + XCTAssertEqual(appLaunchView.attribute(forKey: "attribute"), "value") + } + + func testAddingGlobalAttributeAfterSDKInit_thenSendingInteractiveEvent() throws { + // Given + monitor.notifySDKInit() + monitor.addAttribute(forKey: "attribute", value: "value") + + // When + monitor.addAction(type: .custom, name: "custom action") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName) + XCTAssertNil(lastView.attribute(forKey: "attribute")) + } + + func testAddingGlobalAttributesAfterSDKInit_thenRemovingAttribute() throws { + // Given + monitor.notifySDKInit() + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.removeAttribute(forKey: "attribute1") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName) + XCTAssertNil(lastView.attribute(forKey: "attribute1")) + XCTAssertNil(lastView.attribute(forKey: "attribute2")) + } + + func testAddingGlobalAttributeAfterSDKInit_thenRemovingAttributeAndStartingView() throws { + // Given + monitor.notifySDKInit() + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.removeAttribute(forKey: "attribute1") + monitor.startView(key: "View") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertNil(lastView.attribute(forKey: "attribute1")) + XCTAssertEqual(lastView.attribute(forKey: "attribute2"), "value2") + } + + func testAddingGlobalAttributeAfterSDKInit_thenRemovingAttributeAndStartingAndStoppingView() throws { + // Given + monitor.notifySDKInit() + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.removeAttribute(forKey: "attribute1") + monitor.startView(key: "View") + monitor.stopView(key: "View") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertNil(lastView.attribute(forKey: "attribute1")) + XCTAssertEqual(lastView.attribute(forKey: "attribute2"), "value2") + } + + func testUpdatingGlobalAttributesAfterSDKInit_thenStartingAndStoppingView() throws { + // Given + monitor.notifySDKInit() + + // When + monitor.addAttribute(forKey: "attribute", value: "value") + monitor.addAttribute(forKey: "attribute", value: "new-value") + monitor.removeAttribute(forKey: "unknown-attribute") + monitor.startView(key: "View") + monitor.stopView(key: "View") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertEqual(lastView.attribute(forKey: "attribute"), "new-value") + XCTAssertEqual(lastView.numberOfAttributes, 1) + } + + // MARK: - Changing Global Attributes After Starting View + + func testAddingGlobalAttributeAfterViewIsStarted() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View") + + // When + monitor.addAttribute(forKey: "attribute", value: "value") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertNil(lastView.attribute(forKey: "attribute")) + } + + func testAddingGlobalAttributeAfterViewIsStarted_thenSendingInteractiveEvent() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View") + monitor.addAttribute(forKey: "attribute", value: "value") + + // When + monitor.addAction(type: .custom, name: "custom action") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertNil(lastView.attribute(forKey: "attribute")) + } + + func testAddingGlobalAttributesAfterViewIsStarted_thenRemovingAttribute() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View") + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.removeAttribute(forKey: "attribute1") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertNil(lastView.attribute(forKey: "attribute1")) + XCTAssertNil(lastView.attribute(forKey: "attribute2")) + } + + func testAddingGlobalAttributesAfterViewIsStarted_thenRemovingAttributeAndStoppingView() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View") + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.removeAttribute(forKey: "attribute1") + monitor.stopView(key: "View") + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + XCTAssertEqual(lastView.view.name, "View") + XCTAssertNil(lastView.attribute(forKey: "attribute1")) + XCTAssertEqual(lastView.attribute(forKey: "attribute2"), "value2") + } + + func testAddingGlobalAttributeAfterViewIsStarted_thenStartingNextViewsWhileAddingAttributes() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View1") + monitor.addAttribute(forKey: "attribute1", value: "value1") + + // When + monitor.startView(key: "View2") + monitor.addAttribute(forKey: "attribute2", value: "value2") + monitor.startView(key: "View3") + monitor.addAttribute(forKey: "attribute3", value: "value3") + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let lastView1 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View1" })) + let lastView2 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View2" })) + let lastView3 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View3" })) + + XCTAssertEqual(lastView1.attribute(forKey: "attribute1"), "value1") + XCTAssertNil(lastView1.attribute(forKey: "attribute2")) + XCTAssertNil(lastView1.attribute(forKey: "attribute3")) + + XCTAssertEqual(lastView2.attribute(forKey: "attribute1"), "value1") + XCTAssertEqual(lastView2.attribute(forKey: "attribute2"), "value2") + XCTAssertNil(lastView2.attribute(forKey: "attribute3")) + + XCTAssertEqual(lastView3.attribute(forKey: "attribute1"), "value1") + XCTAssertEqual(lastView3.attribute(forKey: "attribute2"), "value2") + XCTAssertNil(lastView2.attribute(forKey: "attribute3")) + } + + func testAddingGlobalAttributesAfterViewIsStarted_thenStartingNextViewsWhileRemovingAttributes() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View1") + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + monitor.addAttribute(forKey: "attribute3", value: "value3") + + // When + monitor.startView(key: "View2") + monitor.removeAttribute(forKey: "attribute1") + monitor.startView(key: "View3") + monitor.removeAttribute(forKey: "attribute2") + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let lastView1 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View1" })) + let lastView2 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View2" })) + let lastView3 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View3" })) + + XCTAssertEqual(lastView1.attribute(forKey: "attribute1"), "value1") + XCTAssertEqual(lastView1.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(lastView1.attribute(forKey: "attribute3"), "value3") + + XCTAssertEqual(lastView2.attribute(forKey: "attribute1"), "value1") + XCTAssertEqual(lastView2.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(lastView2.attribute(forKey: "attribute3"), "value3") + + XCTAssertNil(lastView3.attribute(forKey: "attribute1")) + XCTAssertEqual(lastView3.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(lastView3.attribute(forKey: "attribute3"), "value3") + } + + func testAddingGlobalAttributesAfterViewIsStarted_thenStartingNextViewsWhileUpdatingAttributes() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "View1") + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + monitor.addAttribute(forKey: "attribute3", value: "value3") + + // When + monitor.startView(key: "View2") + monitor.addAttribute(forKey: "attribute1", value: "new-value1") + monitor.startView(key: "View3") + monitor.addAttribute(forKey: "attribute2", value: "new-value1") + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let lastView1 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View1" })) + let lastView2 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View2" })) + let lastView3 = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "View3" })) + + XCTAssertEqual(lastView1.attribute(forKey: "attribute1"), "value1") + XCTAssertEqual(lastView1.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(lastView1.attribute(forKey: "attribute3"), "value3") + + XCTAssertEqual(lastView2.attribute(forKey: "attribute1"), "new-value1") + XCTAssertEqual(lastView2.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(lastView2.attribute(forKey: "attribute3"), "value3") + + XCTAssertEqual(lastView3.attribute(forKey: "attribute1"), "new-value1") + XCTAssertEqual(lastView3.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(lastView3.attribute(forKey: "attribute3"), "value3") + } + + // MARK: - Changing Global Attributes While There Is An Inactive View + + func testAddingGlobalAttributeWhileInactiveView_thenImplicitlyStoppingInactiveView() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "InactiveView") + monitor.startResource(resourceKey: "pending resource", url: .mockAny()) + monitor.startView(key: "ActiveView") + + // When + monitor.addAttribute(forKey: "attribute", value: "value") + monitor.stopResource(resourceKey: "pending resource", response: .mockAny()) + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let lastInactiveView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "InactiveView" })) + let lastActiveView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "ActiveView" })) + + XCTAssertEqual(lastInactiveView.view.resource.count, 1) + XCTAssertNil(lastInactiveView.attribute(forKey: "attribute")) + XCTAssertNil(lastActiveView.attribute(forKey: "attribute")) + } + + func testAddingGlobalAttributesWhileInactiveView_thenRemovingAttributeAndImplicitlyStoppingInactiveView() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "InactiveView") + monitor.startResource(resourceKey: "pending resource", url: .mockAny()) + monitor.startView(key: "ActiveView") + + // When + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + monitor.removeAttribute(forKey: "attribute1") + monitor.stopResource(resourceKey: "pending resource", response: .mockAny()) + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let lastInactiveView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "InactiveView" })) + let lastActiveView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "ActiveView" })) + + XCTAssertEqual(lastInactiveView.view.resource.count, 1) + XCTAssertNil(lastInactiveView.attribute(forKey: "attribute1")) + XCTAssertNil(lastInactiveView.attribute(forKey: "attribute2")) + XCTAssertNil(lastActiveView.attribute(forKey: "attribute1")) + XCTAssertNil(lastActiveView.attribute(forKey: "attribute2")) + } + + func testAddingGlobalAttributesWhileInactiveView_thenRemovingAttributeAndImplicitlyStoppingInactiveViewAndStoppingActiveView() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "InactiveView") + monitor.startResource(resourceKey: "pending resource", url: .mockAny()) + monitor.startView(key: "ActiveView") + + // When + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + monitor.removeAttribute(forKey: "attribute1") + monitor.stopResource(resourceKey: "pending resource", response: .mockAny()) + monitor.stopView(key: "ActiveView") + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self) + let lastInactiveView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "InactiveView" })) + let lastActiveView = try XCTUnwrap(viewEvents.last(where: { $0.view.name == "ActiveView" })) + + XCTAssertEqual(lastInactiveView.view.resource.count, 1) + XCTAssertNil(lastInactiveView.attribute(forKey: "attribute1")) + XCTAssertNil(lastInactiveView.attribute(forKey: "attribute2")) + XCTAssertNil(lastActiveView.attribute(forKey: "attribute1")) + XCTAssertEqual(lastActiveView.attribute(forKey: "attribute2"), "value2") + } + + // MARK: - Including Up-to-date Global Attributes In Events + + func testAddingGlobalAttributeToActiveView_thenCollectingViewEvents() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "ActiveView") + monitor.addAttribute(forKey: "attribute", value: "value") + + // When + monitor.addError(message: "error event") + monitor.addAction(type: .custom, name: "custom action event") + monitor.startResource(resourceKey: "resource", url: .mockAny()) + monitor.stopResource(resourceKey: "resource", response: .mockAny()) + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + let errorEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self).last) + let actionEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMActionEvent.self).last) + let resourceEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMResourceEvent.self).last) + + XCTAssertEqual(lastView.view.error.count, 1) + XCTAssertEqual(lastView.view.action.count, 1) + XCTAssertEqual(lastView.view.resource.count, 1) + XCTAssertNil(lastView.attribute(forKey: "attribute")) + XCTAssertEqual(errorEvent.context?.contextInfo["attribute"] as? String, "value") + XCTAssertEqual(actionEvent.context?.contextInfo["attribute"] as? String, "value") + XCTAssertEqual(resourceEvent.context?.contextInfo["attribute"] as? String, "value") + } + + func testAddingGlobalAttributesToActiveView_thenRemovingAttributeAndCollectingViewEvents() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "ActiveView") + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.removeAttribute(forKey: "attribute1") + monitor.addError(message: "error event") + monitor.addAction(type: .custom, name: "custom action event") + monitor.startResource(resourceKey: "resource", url: .mockAny()) + monitor.stopResource(resourceKey: "resource", response: .mockAny()) + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + let errorEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self).last) + let actionEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMActionEvent.self).last) + let resourceEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMResourceEvent.self).last) + + XCTAssertEqual(lastView.view.error.count, 1) + XCTAssertEqual(lastView.view.action.count, 1) + XCTAssertEqual(lastView.view.resource.count, 1) + XCTAssertNil(lastView.attribute(forKey: "attribute1")) + XCTAssertNil(lastView.attribute(forKey: "attribute2")) + XCTAssertNil(errorEvent.context?.contextInfo["attribute1"]) + XCTAssertEqual(errorEvent.context?.contextInfo["attribute2"] as? String, "value2") + XCTAssertNil(actionEvent.context?.contextInfo["attribute1"]) + XCTAssertEqual(actionEvent.context?.contextInfo["attribute2"] as? String, "value2") + XCTAssertNil(resourceEvent.context?.contextInfo["attribute1"]) + XCTAssertEqual(resourceEvent.context?.contextInfo["attribute2"] as? String, "value2") + } + + func testUpdatingGlobalAttributeInActiveView_thenCollectingViewEvents() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "ActiveView") + monitor.addAttribute(forKey: "attribute", value: "value") + monitor.addAttribute(forKey: "attribute", value: "new-value") + + // When + monitor.addError(message: "error event") + monitor.addAction(type: .custom, name: "custom action event") + monitor.startResource(resourceKey: "resource", url: .mockAny()) + monitor.stopResource(resourceKey: "resource", response: .mockAny()) + + // Then + let lastView = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).last) + let errorEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self).last) + let actionEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMActionEvent.self).last) + let resourceEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMResourceEvent.self).last) + + XCTAssertEqual(lastView.view.error.count, 1) + XCTAssertEqual(lastView.view.action.count, 1) + XCTAssertEqual(lastView.view.resource.count, 1) + XCTAssertNil(lastView.attribute(forKey: "attribute")) + XCTAssertEqual(errorEvent.context?.contextInfo["attribute"] as? String, "new-value") + XCTAssertEqual(actionEvent.context?.contextInfo["attribute"] as? String, "new-value") + XCTAssertEqual(resourceEvent.context?.contextInfo["attribute"] as? String, "new-value") + } + + func testAddingGlobalAttributesToActiveView_thenCollectingViewTimingsAndRemovingAttribute() throws { + // Given + monitor.notifySDKInit() + monitor.startView(key: "ActiveView") + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + + // When + monitor.addTiming(name: "timing1") + monitor.removeAttribute(forKey: "attribute1") + monitor.addTiming(name: "timing2") + + // Then + let viewEvents = featureScope.eventsWritten(ofType: RUMViewEvent.self).filter { $0.view.name == "ActiveView" } + let viewAfterFirstTiming = try XCTUnwrap(viewEvents.last(where: { $0.view.customTimings?.count == 1 })) + let viewAfterSecondTiming = try XCTUnwrap(viewEvents.last(where: { $0.view.customTimings?.count == 2 })) + + XCTAssertEqual(viewAfterFirstTiming.attribute(forKey: "attribute1"), "value1") + XCTAssertEqual(viewAfterFirstTiming.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(viewAfterSecondTiming.attribute(forKey: "attribute2"), "value2") + XCTAssertEqual(viewAfterSecondTiming.attribute(forKey: "attribute2"), "value2") + } + + // MARK: - Updating Fatal Error Context With Global Attributes + + func testGivenSDKInitialized_whenGlobalAttributeIsAdded_thenFatalErrorContextIsUpdatedWithNewAttributes() throws { + let fatalErrorContext = FatalErrorContextNotifierMock() + + // Given + monitor = Monitor( + dependencies: .mockWith(featureScope: featureScope, fatalErrorContext: fatalErrorContext), + dateProvider: SystemDateProvider() + ) + monitor.notifySDKInit() + + // When + monitor.addAttribute(forKey: "attribute", value: "value") + + // Then + XCTAssertEqual(fatalErrorContext.globalAttributes["attribute"] as? String, "value") + XCTAssertEqual(fatalErrorContext.globalAttributes.count, 1) + } + + func testGivenSDKInitialized_whenGlobalAttributesAreAddedAndRemoved_thenFatalErrorContextIsUpdatedWithNewAttributes() throws { + let fatalErrorContext = FatalErrorContextNotifierMock() + + // Given + monitor = Monitor( + dependencies: .mockWith(featureScope: featureScope, fatalErrorContext: fatalErrorContext), + dateProvider: SystemDateProvider() + ) + monitor.notifySDKInit() + + // When + monitor.addAttribute(forKey: "attribute1", value: "value1") + monitor.addAttribute(forKey: "attribute2", value: "value2") + monitor.removeAttribute(forKey: "attribute1") + + // Then + XCTAssertEqual(fatalErrorContext.globalAttributes["attribute2"] as? String, "value2") + XCTAssertEqual(fatalErrorContext.globalAttributes.count, 1) + } +} + +// MARK: - Helpers + +private extension RUMViewEvent { + func attribute(forKey key: String) -> Any? { + return context?.contextInfo[key] + } + + func attribute(forKey key: String) -> T? { + return context?.contextInfo[key] as? T + } + + var numberOfAttributes: Int { + return context?.contextInfo.count ?? 0 + } +} diff --git a/DatadogRUM/Tests/RUMMonitor/MonitorTests.swift b/DatadogRUM/Tests/RUMMonitor/MonitorTests.swift index d0cefe3f7f..5ceb74335b 100644 --- a/DatadogRUM/Tests/RUMMonitor/MonitorTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/MonitorTests.swift @@ -22,7 +22,6 @@ class MonitorTests: XCTestCase { dateProvider: DateProviderMock() ) monitor.startView(key: "foo") - monitor.flush() // Then let expectedContext = monitor.currentRUMContext @@ -43,7 +42,6 @@ class MonitorTests: XCTestCase { dateProvider: DateProviderMock() ) monitor.startView(key: "foo") - monitor.flush() // Then XCTAssertNil(featureScope.contextMock.baggages[RUMFeature.name]) @@ -59,7 +57,6 @@ class MonitorTests: XCTestCase { dateProvider: DateProviderMock() ) monitor.startView(viewController: vc) - monitor.flush() // Then XCTAssertEqual(monitor.scopes.sessionScopes.first?.viewScopes.first?.viewName, "SomeViewController") @@ -76,7 +73,6 @@ class MonitorTests: XCTestCase { dateProvider: DateProviderMock() ) monitor.startView(viewController: vc, name: "Some View") - monitor.flush() // Then XCTAssertEqual(monitor.scopes.sessionScopes.first?.viewScopes.first?.viewName, "Some View") diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/FatalErrorContextNotifierTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/FatalErrorContextNotifierTests.swift new file mode 100644 index 0000000000..e025b0e144 --- /dev/null +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/FatalErrorContextNotifierTests.swift @@ -0,0 +1,96 @@ +/* + * 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 DatadogInternal +import TestUtilities +@testable import DatadogRUM + +class FatalErrorContextNotifierTests: XCTestCase { + private let featureScope = FeatureScopeMock() + + // MARK: - Changing Session State + + func testWhenSessionStateIsSet_itSendsSessionStateMessage() throws { + // Given + let fatalErrorContext = FatalErrorContextNotifier(messageBus: featureScope) + let newSessionState: RUMSessionState = .mockRandom() + + // When + fatalErrorContext.sessionState = newSessionState + + // Then + let messages = featureScope.messagesSent() + XCTAssertEqual(messages.count, 1) + let sessionStateMessage = try XCTUnwrap(messages.lastBaggage(withKey: RUMBaggageKeys.sessionState)) + XCTAssertEqual(newSessionState, try sessionStateMessage.decode()) + } + + func testWhenSessionStateIsReset_itDoesNotSendNextSessionStateMessage() throws { + // Given + let fatalErrorContext = FatalErrorContextNotifier(messageBus: featureScope) + let originalSessionState: RUMSessionState = .mockRandom() + fatalErrorContext.sessionState = originalSessionState + + // When + fatalErrorContext.sessionState = nil + + // Then + let messages = featureScope.messagesSent() + XCTAssertEqual(messages.count, 1) + let sessionStateMessage = try XCTUnwrap(messages.lastBaggage(withKey: RUMBaggageKeys.sessionState)) + XCTAssertEqual(originalSessionState, try sessionStateMessage.decode()) + } + + // MARK: - Changing View State + + func testWhenViewIsSet_itSendsViewEventMessage() throws { + // Given + let fatalErrorContext = FatalErrorContextNotifier(messageBus: featureScope) + let newViewEvent: RUMViewEvent = .mockRandom() + + // When + fatalErrorContext.view = newViewEvent + + // Then + let messages = featureScope.messagesSent() + XCTAssertEqual(messages.count, 1) + let viewEventMessage = try XCTUnwrap(messages.lastBaggage(withKey: RUMBaggageKeys.viewEvent)) + DDAssertJSONEqual(newViewEvent, try viewEventMessage.decode(type: RUMViewEvent.self)) + } + + func testWhenViewIsReset_itSendsViewResetMessage() throws { + // Given + let fatalErrorContext = FatalErrorContextNotifier(messageBus: featureScope) + fatalErrorContext.view = .mockRandom() + + // When + fatalErrorContext.view = nil + + // Then + let messages = featureScope.messagesSent() + XCTAssertEqual(messages.count, 2) + let viewResetMessage = try XCTUnwrap(messages.lastBaggage(withKey: RUMBaggageKeys.viewReset)) + XCTAssertTrue(try viewResetMessage.decode(type: Bool.self)) + } + + // MARK: - Changing Global Attributes + + func testWhenGlobalAttributesAreSet_itSendsAttributesMessage() throws { + // Given + let fatalErrorContext = FatalErrorContextNotifier(messageBus: featureScope) + let newGlobalAttributes = mockRandomAttributes() + + // When + fatalErrorContext.globalAttributes = newGlobalAttributes + + // Then + let messages = featureScope.messagesSent() + XCTAssertEqual(messages.count, 1) + let attributesMessage = try XCTUnwrap(messages.lastBaggage(withKey: RUMBaggageKeys.attributes)) + DDAssertJSONEqual(newGlobalAttributes, try attributesMessage.decode(type: GlobalRUMAttributes.self).attributes) + } +} diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMResourceScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMResourceScopeTests.swift index 055574d884..5adb8dc624 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMResourceScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMResourceScopeTests.swift @@ -1174,6 +1174,43 @@ class RUMResourceScopeTests: XCTestCase { XCTAssertEqual(event.error.sourceType, .reactNative) } + func testGivenStartedResource_whenResourceLoadingEndsWithError_itSendsErrorEventWithTimeSinceAppStart() throws { + let currentTime: Date = .mockDecember15th2019At10AMUTC() + + // Given + let appLauchToErrorTimeDiff = Int64.random(in: 10..<1_000_000) + let customContext: DatadogContext = .mockWith(launchTime: .mockWith( + launchTime: .mockAny(), + launchDate: currentTime, + isActivePrewarm: .mockAny() + ) ) + + let scope = RUMResourceScope.mockWith( + context: rumContext, + dependencies: dependencies, + resourceKey: "/resource/1", + startTime: currentTime, + url: "https://foo.com/resource/1", + httpMethod: .post + ) + + // When + XCTAssertFalse( + scope.process( + command: RUMStopResourceWithErrorCommand.mockWithErrorMessage( + resourceKey: "/resource/1", + time: currentTime.addingTimeInterval(Double(appLauchToErrorTimeDiff)) + ), + context: customContext, + writer: writer + ) + ) + + // Then + let event = try XCTUnwrap(writer.events(ofType: RUMErrorEvent.self).first) + XCTAssertEqual(event.error.timeSinceAppStart, appLauchToErrorTimeDiff * 1_000) + } + // MARK: - Events sending callbacks func testGivenResourceScopeWithDefaultEventsMapper_whenSendingEvents_thenEventSentCallbacksAreCalled() throws { diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 48d777a8ad..fef13b0671 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -336,10 +336,11 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual(scope.viewScopes.count, 0) } - // MARK: - Sending Messages Over Message Bus + // MARK: - Updating Fatal Error Context - func testWhenSessionScopeIsCreated_itSendsSessionStateMessage() throws { + func testWhenSessionScopeIsCreated_itUpdatesFatalErrorContextWithSessionState() throws { let featureScope = FeatureScopeMock() + let fatalErrorContext = FatalErrorContextNotifierMock() let randomIsInitialSession: Bool = .mockRandom() let randomIsReplayBeingRecorded: Bool? = .mockRandom() @@ -349,7 +350,8 @@ class RUMSessionScopeTests: XCTestCase { parent: parent, dependencies: .mockWith( featureScope: featureScope, - sessionSampler: Bool.random() ? .mockKeepAll() : .mockRejectAll() // no matter if sampled or not + sessionSampler: Bool.random() ? .mockKeepAll() : .mockRejectAll(), // no matter if sampled or not, + fatalErrorContext: fatalErrorContext ), hasReplay: randomIsReplayBeingRecorded ) @@ -361,13 +363,13 @@ class RUMSessionScopeTests: XCTestCase { hasTrackedAnyView: false, didStartWithReplay: randomIsReplayBeingRecorded ) - let baggageSent = try XCTUnwrap(featureScope.messagesSent().lastBaggage(withKey: RUMBaggageKeys.sessionState)) - let sessionStateSent: RUMSessionState = try baggageSent.decode() - XCTAssertEqual(sessionStateSent, expectedSessionState, "It must send 'session state' message") + let actualSessionState = try XCTUnwrap(fatalErrorContext.sessionState) + XCTAssertEqual(actualSessionState, expectedSessionState) } - func testWhenSessionScopeStartsAnyView_itSendsSessionStateMessage() throws { + func testWhenSessionScopeStartsAnyView_itUpdatesFatalErrorContextWithSessionState() throws { let featureScope = FeatureScopeMock() + let fatalErrorContext = FatalErrorContextNotifierMock() let randomIsInitialSession: Bool = .mockRandom() let randomIsReplayBeingRecorded: Bool? = .mockRandom() @@ -377,7 +379,10 @@ class RUMSessionScopeTests: XCTestCase { isInitialSession: randomIsInitialSession, parent: parent, startTime: sessionStartTime, - dependencies: .mockWith(featureScope: featureScope), + dependencies: .mockWith( + featureScope: featureScope, + fatalErrorContext: fatalErrorContext + ), hasReplay: randomIsReplayBeingRecorded ) @@ -393,20 +398,23 @@ class RUMSessionScopeTests: XCTestCase { hasTrackedAnyView: true, didStartWithReplay: randomIsReplayBeingRecorded ) - let baggageSent = try XCTUnwrap(featureScope.messagesSent().lastBaggage(withKey: RUMBaggageKeys.sessionState)) - let sessionStateSent: RUMSessionState = try baggageSent.decode() - XCTAssertEqual(sessionStateSent, expectedSessionState, "It must send 'session state' message") + let actualSessionState = try XCTUnwrap(fatalErrorContext.sessionState) + XCTAssertEqual(actualSessionState, expectedSessionState) } - func testWhenSessionScopeHasNoActiveView_itSendsViewEventMessages() throws { + func testWhenSessionScopeHasNoActiveView_itUpdatesFatalErrorContextWithView() throws { let featureScope = FeatureScopeMock() + let fatalErrorContext = FatalErrorContextNotifierMock() // Given let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( parent: parent, startTime: sessionStartTime, - dependencies: .mockWith(featureScope: featureScope) + dependencies: .mockWith( + featureScope: featureScope, + fatalErrorContext: fatalErrorContext + ) ) // When @@ -414,19 +422,42 @@ class RUMSessionScopeTests: XCTestCase { _ = scope.process(command: command, context: context, writer: writer) // Then - let viewEventBaggage: RUMViewEvent = try XCTUnwrap( - featureScope.messagesSent().lastBaggage(withKey: RUMBaggageKeys.viewEvent)?.decode() - ) - XCTAssertEqual(viewEventBaggage.view.name, "ActiveView", "It must send 'view event' message") + XCTAssertEqual(fatalErrorContext.view?.view.name, "ActiveView") // When _ = scope.process(command: RUMStopViewCommand.mockWith(time: sessionStartTime.addingTimeInterval(1), identity: .mockViewIdentifier()), context: context, writer: writer) // Then - let viewResetBaggage: Bool = try XCTUnwrap( - featureScope.messagesSent().lastBaggage(withKey: RUMBaggageKeys.viewReset)?.decode() + XCTAssertNil(fatalErrorContext.view) + } + + func testWhenSessionEnds_itUpdatesFatalErrorContextWithView() throws { + let featureScope = FeatureScopeMock() + let fatalErrorContext = FatalErrorContextNotifierMock() + + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date(), + dependencies: .mockWith( + featureScope: featureScope, + fatalErrorContext: fatalErrorContext + ) ) - XCTAssertTrue(viewResetBaggage, "It must send 'view reset' message") + + let command = RUMStartViewCommand.mockWith(time: Date(), identity: .mockViewIdentifier(), name: "ActiveView") + + // When + _ = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertEqual(fatalErrorContext.view?.view.name, "ActiveView") + + // When + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + + // Then + XCTAssertNil(fatalErrorContext.view) } // MARK: - Stopping Sessions @@ -557,60 +588,6 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertFalse(result) } - func testWhenScopeEnded_itSendsViewEventMessages() throws { - let featureScope = FeatureScopeMock() - - // Given - let scope: RUMSessionScope = .mockWith( - parent: parent, - startTime: Date(), - dependencies: .mockWith(featureScope: featureScope) - ) - - let command = RUMStartViewCommand.mockWith(time: Date(), identity: .mockViewIdentifier(), name: "ActiveView") - // When - _ = scope.process(command: command, context: context, writer: writer) - // Then - let viewEventBaggage: RUMViewEvent = try XCTUnwrap( - featureScope.messagesSent().lastBaggage(withKey: RUMBaggageKeys.viewEvent)?.decode() - ) - XCTAssertEqual(viewEventBaggage.view.name, "ActiveView", "It must send 'view event' message") - - // When - _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) - - // Then - let viewResetBaggage: Bool = try XCTUnwrap( - featureScope.messagesSent().lastBaggage(withKey: RUMBaggageKeys.viewReset)?.decode() - ) - XCTAssertTrue(viewResetBaggage, "It must send 'view reset' message") - } - - func testWhenScopeEnded_itDoesNotSendViewResetMessage() { - let featureScope = FeatureScopeMock() - - // Given - let scope: RUMSessionScope = .mockWith( - parent: parent, - startTime: Date(), - dependencies: .mockWith(featureScope: featureScope) - ) - - let startViewCommand = RUMStartViewCommand.mockWith(time: Date(), identity: .mockViewIdentifier()) - _ = scope.process(command: startViewCommand, context: context, writer: writer) - let startResourceCommand = RUMStartResourceCommand.mockWith(time: Date()) - _ = scope.process(command: startResourceCommand, context: context, writer: writer) - - // When - _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) - let stopResourceCommand = RUMStopResourceCommand.mockWith(resourceKey: startResourceCommand.resourceKey, time: Date()) - _ = scope.process(command: stopResourceCommand, context: context, writer: writer) - - // Then - let viewResetMessages = featureScope.messagesSent().filter { $0.asBaggage?.key == RUMBaggageKeys.viewReset } - XCTAssertEqual(viewResetMessages.count, 1, "It must send only one 'view reset' message") - } - // MARK: - Usage func testGivenSessionWithNoActiveScope_whenReceivingRUMCommandOtherThanKeepSessionAliveCommand_itLogsWarning() throws { diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMViewScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMViewScopeTests.swift index 0634533f61..33bd8a0ecb 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -1557,6 +1557,74 @@ class RUMViewScopeTests: XCTestCase { XCTAssertNil(error.context!.contextInfo[RUM.Attributes.errorFingerprint]) } + func testGivenStartedView_whenErrorWithIncludeBinaryImagesAttributesIsAdded_itAddsBinaryImagesToError() throws { + // Given + let mockBacktrace: BacktraceReport = .mockRandom() + let hasReplay: Bool = .mockRandom() + var context = self.context + context.baggages = try .mockSessionReplayAttributes(hasReplay: hasReplay) + var currentTime: Date = .mockDecember15th2019At10AMUTC() + + let scope = RUMViewScope( + isInitialView: .mockRandom(), + parent: parent, + dependencies: .mockWith( + backtraceReporter: BacktraceReporterMock(backtrace: mockBacktrace) + ), + identity: .mockViewIdentifier(), + path: "UIViewController", + name: "ViewName", + attributes: [:], + customTimings: [:], + startTime: currentTime, + serverTimeOffset: .zero + ) + + XCTAssertTrue( + scope.process( + command: RUMStartViewCommand.mockWith(time: currentTime, attributes: [:], identity: .mockViewIdentifier()), + context: context, + writer: writer + ) + ) + + currentTime.addTimeInterval(1) + + // When + XCTAssertTrue( + scope.process( + command: RUMAddCurrentViewErrorCommand.mockWithErrorMessage( + time: currentTime, + message: "view error", + source: .source, + stack: nil, + attributes: [ + CrossPlatformAttributes.includeBinaryImages: true + ] + ), + context: context, + writer: writer + ) + ) + + // Then + let error = try XCTUnwrap(writer.events(ofType: RUMErrorEvent.self).last) + XCTAssertNil(error.context?.contextInfo[CrossPlatformAttributes.includeBinaryImages]) + XCTAssertNotNil(error.error.binaryImages) + XCTAssertEqual(error.error.binaryImages?.count, mockBacktrace.binaryImages.count) + for i in 0.. "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", diff --git a/DatadogSDKAlamofireExtension.podspec b/DatadogSDKAlamofireExtension.podspec index 6b854e0a29..dc74781193 100644 --- a/DatadogSDKAlamofireExtension.podspec +++ b/DatadogSDKAlamofireExtension.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = "DatadogSDKAlamofireExtension" s.module_name = "DatadogAlamofireExtension" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", diff --git a/DatadogSDKCrashReporting.podspec b/DatadogSDKCrashReporting.podspec index ff521ab0c8..e3fd72abd6 100644 --- a/DatadogSDKCrashReporting.podspec +++ b/DatadogSDKCrashReporting.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = "DatadogSDKCrashReporting" s.module_name = "DatadogCrashReporting" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.source_files = "DatadogCrashReporting/Sources/**/*.swift" s.dependency 'DatadogInternal', s.version.to_s - s.dependency 'PLCrashReporter', '~> 1.11.1' + s.dependency 'PLCrashReporter', '~> 1.11.2' s.resource_bundle = { "DatadogCrashReporting" => "DatadogCrashReporting/Resources/PrivacyInfo.xcprivacy" diff --git a/DatadogSDKObjc.podspec b/DatadogSDKObjc.podspec index bdce91e477..980f790bfa 100644 --- a/DatadogSDKObjc.podspec +++ b/DatadogSDKObjc.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = "DatadogSDKObjc" s.module_name = "DatadogObjc" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Official Datadog Objective-C SDK for iOS." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maciej Burda" => "maciej.burda@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", diff --git a/DatadogSessionReplay.podspec b/DatadogSessionReplay.podspec index 2a32907f3c..92c1cc5593 100644 --- a/DatadogSessionReplay.podspec +++ b/DatadogSessionReplay.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "DatadogSessionReplay" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Official Datadog Session Replay SDK for iOS." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maciej Burda" => "maciej.burda@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '11.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } - + s.source_files = ["DatadogSessionReplay/Sources/**/*.swift"] s.dependency 'DatadogInternal', s.version.to_s end diff --git a/DatadogSessionReplay/Sources/Recorder/Recorder.swift b/DatadogSessionReplay/Sources/Recorder/Recorder.swift index bc15a36ca8..a216f7cf3b 100644 --- a/DatadogSessionReplay/Sources/Recorder/Recorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/Recorder.swift @@ -99,7 +99,7 @@ public class Recorder: Recording { snapshotProcessor: SnapshotProcessing, resourceProcessor: ResourceProcessing, telemetry: Telemetry, - methodCallTelemetrySamplingRate: Float = 5 + methodCallTelemetrySamplingRate: Float = 0.1 ) { self.uiApplicationSwizzler = uiApplicationSwizzler self.viewTreeSnapshotProducer = viewTreeSnapshotProducer @@ -123,7 +123,7 @@ public class Recorder: Recording { let methodCalledTrace = telemetry.startMethodCalled( operationName: MethodCallConstants.captureRecordOperationName, callerClass: MethodCallConstants.className, - samplingRate: methodCallTelemetrySamplingRate // Effectively 3% * 5% = 0.15% of calls + samplingRate: methodCallTelemetrySamplingRate // Effectively 3% * 0.1% = 0.003% of calls ) var isSuccessful = true do { diff --git a/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift b/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift index d468db3e88..86bacd905e 100644 --- a/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Feature/RequestBuilder/SegmentRequestBuilderTests.swift @@ -247,12 +247,13 @@ class SegmentRequestBuilderTests: XCTestCase { _ = try builder.request(for: mockEvents, with: .mockWith(source: "invalid source")) // Then - XCTAssertEqual( - telemetry.description, - """ - Telemetry logs: - - [error] [SR] Could not create segment source from provided string 'invalid source', kind: nil, stack: nil - """ + XCTAssertTrue( + telemetry.description.hasPrefix( + """ + Telemetry logs: + - [error] [SR] Could not create segment source from provided string 'invalid source' + """ + ) ) } } diff --git a/DatadogSessionReplay/Tests/SessionReplayTests.swift b/DatadogSessionReplay/Tests/SessionReplayTests.swift index c724a8e392..0b5ccf70a9 100644 --- a/DatadogSessionReplay/Tests/SessionReplayTests.swift +++ b/DatadogSessionReplay/Tests/SessionReplayTests.swift @@ -84,10 +84,9 @@ class SessionReplayTests: XCTestCase { ) // Then - let message = messageReceiver.messages.firstTelemetry() - let configuration = message?.asConfiguration - XCTAssertEqual(configuration?.sessionReplaySampleRate, sampleRate) - XCTAssertEqual(configuration?.defaultPrivacyLevel, privacyLevel.rawValue) + let configuration = try XCTUnwrap(messageReceiver.messages.firstTelemetry?.asConfiguration) + XCTAssertEqual(configuration.sessionReplaySampleRate, sampleRate) + XCTAssertEqual(configuration.defaultPrivacyLevel, privacyLevel.rawValue) } func testWhenEnabledWithReplaySampleRate() throws { diff --git a/DatadogTrace.podspec b/DatadogTrace.podspec index 2542d0c910..ed7e11024f 100644 --- a/DatadogTrace.podspec +++ b/DatadogTrace.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "DatadogTrace" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Datadog Trace Module." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maciej Burda" => "maciej.burda@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", @@ -19,9 +19,9 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '11.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } - + s.source_files = ["DatadogTrace/Sources/**/*.swift"] s.dependency 'DatadogInternal', s.version.to_s - + s.dependency 'OpenTelemetrySwiftApi', '1.6.0' end diff --git a/DatadogTrace/Sources/DDNoOps.swift b/DatadogTrace/Sources/DDNoOps.swift index 1f013b0161..85730d6e51 100644 --- a/DatadogTrace/Sources/DDNoOps.swift +++ b/DatadogTrace/Sources/DDNoOps.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import OpenTelemetryApi internal struct DDNoopGlobals { static let tracer = DDNoopTracer() @@ -13,7 +14,7 @@ internal struct DDNoopGlobals { static let context = DDNoopSpanContext() } -internal struct DDNoopTracer: OTTracer { +internal class DDNoopTracer: OTTracer, OpenTelemetryApi.Tracer { var activeSpan: OTSpan? = nil private func warn() { @@ -44,6 +45,13 @@ internal struct DDNoopTracer: OTTracer { warn() return DDNoopGlobals.span } + + // MARK: - Open Telemetry + + func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder { + warn() + return NOPOTelSpanBuilder() + } } internal struct DDNoopSpan: OTSpan { diff --git a/DatadogTrace/Sources/DDSpan.swift b/DatadogTrace/Sources/DDSpan.swift index b25dbb6779..4996b57aa1 100644 --- a/DatadogTrace/Sources/DDSpan.swift +++ b/DatadogTrace/Sources/DDSpan.swift @@ -105,11 +105,15 @@ internal final class DDSpan: OTSpan { } func log(fields: [String: Encodable], timestamp: Date) { + log(message: nil, fields: fields, timestamp: timestamp) + } + + func log(message: String?, fields: [String: Encodable], timestamp: Date) { if warnIfFinished("log(fields:timestamp:)") { return } logFields.append(fields) - sendSpanLogs(fields: fields, date: timestamp) + sendSpanLogs(message: message, fields: fields, date: timestamp) } func finish(at time: Date) { @@ -149,8 +153,8 @@ internal final class DDSpan: OTSpan { } } - private func sendSpanLogs(fields: [String: Encodable], date: Date) { - loggingIntegration.writeLog(withSpanContext: ddContext, fields: fields, date: date, else: { + private func sendSpanLogs(message: String?, fields: [String: Encodable], date: Date) { + loggingIntegration.writeLog(withSpanContext: ddContext, message: message, fields: fields, date: date, else: { DD.logger.warn("The log for span \"\(self.operationName)\" will not be send, because the Logs feature is not enabled.") }) } diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index eb77ea4af7..cc85b73597 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -6,10 +6,11 @@ import Foundation import DatadogInternal +import OpenTelemetryApi -internal class DatadogTracer: OTTracer { +internal final class DatadogTracer: OTTracer, OpenTelemetryApi.Tracer { /// Trace feature scope. - private let featureScope: FeatureScope + let featureScope: FeatureScope /// Global tags configured for Trace feature. let tags: [String: Encodable] @@ -177,4 +178,17 @@ internal class DatadogTracer: OTTracer { forKey: SpanCoreContext.key ) } + // MARK: - OpenTelemetry + + func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder { + OTelSpanBuilder( + active: false, + attributes: [:], + parent: .currentSpan, + spanKind: .internal, + spanName: spanName, + startTime: nil, + tracer: self + ) + } } diff --git a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift index 9dec872ec5..8a30ed8e0a 100644 --- a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift +++ b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift @@ -14,6 +14,8 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { let distributedTraceSampler: Sampler /// First party hosts defined by the user. let firstPartyHosts: FirstPartyHosts + /// Trace context injection configuration to determine whether the trace context should be injected or not. + let traceContextInjection: TraceContextInjection weak var tracer: DatadogTracer? @@ -21,12 +23,14 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { tracer: DatadogTracer, contextReceiver: ContextMessageReceiver, distributedTraceSampler: Sampler, - firstPartyHosts: FirstPartyHosts + firstPartyHosts: FirstPartyHosts, + traceContextInjection: TraceContextInjection ) { self.tracer = tracer self.contextReceiver = contextReceiver self.distributedTraceSampler = distributedTraceSampler self.firstPartyHosts = firstPartyHosts + self.traceContextInjection = traceContextInjection } func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { @@ -55,21 +59,24 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(samplingStrategy: .headBased) + writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: traceContextInjection) case .b3: writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .single + injectEncoding: .single, + traceContextInjection: traceContextInjection ) case .b3multi: writer = B3HTTPHeadersWriter( samplingStrategy: .headBased, - injectEncoding: .multiple + injectEncoding: .multiple, + traceContextInjection: traceContextInjection ) case .tracecontext: writer = W3CHTTPHeadersWriter( samplingStrategy: .headBased, - tracestate: [:] + tracestate: [:], + traceContextInjection: traceContextInjection ) } @@ -84,7 +91,7 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { } } - return (request, (hasSetAnyHeader && injectedSpanContext.isKept) ? injectedSpanContext : nil) + return (request, hasSetAnyHeader ? injectedSpanContext : nil) } func interceptionDidStart(interception: DatadogInternal.URLSessionTaskInterception) { diff --git a/DatadogTrace/Sources/Integrations/TracingWithLoggingIntegration.swift b/DatadogTrace/Sources/Integrations/TracingWithLoggingIntegration.swift index 2c8a282615..3e0348a671 100644 --- a/DatadogTrace/Sources/Integrations/TracingWithLoggingIntegration.swift +++ b/DatadogTrace/Sources/Integrations/TracingWithLoggingIntegration.swift @@ -60,7 +60,14 @@ internal struct TracingWithLoggingIntegration { self.networkInfoEnabled = networkInfoEnabled } - func writeLog(withSpanContext spanContext: DDSpanContext, fields: [String: Encodable], date: Date, else fallback: @escaping () -> Void) { + // swiftlint:disable function_default_parameter_at_end + func writeLog( + withSpanContext spanContext: DDSpanContext, + message: String? = nil, + fields: [String: Encodable], + date: Date, + else fallback: @escaping () -> Void + ) { guard let core = core else { return } @@ -69,7 +76,7 @@ internal struct TracingWithLoggingIntegration { // get the log message and optional error kind let errorKind = userAttributes.removeValue(forKey: OTLogFields.errorKind) as? String - let message = (userAttributes.removeValue(forKey: OTLogFields.message) as? String) ?? Constants.defaultLogMessage + let message = (userAttributes.removeValue(forKey: OTLogFields.message) as? String) ?? message ?? Constants.defaultLogMessage let errorStack = userAttributes.removeValue(forKey: OTLogFields.stack) as? String // infer the log level @@ -107,4 +114,5 @@ internal struct TracingWithLoggingIntegration { else: fallback ) } + // swiftlint:enable function_default_parameter_at_end } diff --git a/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpan.swift b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpan.swift new file mode 100644 index 0000000000..76e2705483 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpan.swift @@ -0,0 +1,47 @@ +/* +* 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 OpenTelemetryApi + +internal class NOPOTelSpan: Span { + var kind: OpenTelemetryApi.SpanKind = .internal + + var name: String = "" + + var context = SpanContext.create( + traceId: TraceId.invalid, + spanId: SpanId.invalid, + traceFlags: TraceFlags(), + traceState: TraceState() + ) + + var isRecording = false + + var status = Status.unset + + var description: String = "OTelNoOpSpan" + + func updateName(name: String) {} + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue?) {} + + func addEvent(name: String) {} + + func addEvent(name: String, timestamp: Date) {} + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue]) {} + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue], timestamp: Date) {} + + func end() { + OpenTelemetry.instance.contextProvider.removeContextForSpan(self) + } + + func end(time: Date) { + end() + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpanBuilder.swift b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpanBuilder.swift new file mode 100644 index 0000000000..d20796a70a --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpanBuilder.swift @@ -0,0 +1,58 @@ +/* +* 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 OpenTelemetryApi + +internal class NOPOTelSpanBuilder: SpanBuilder { + @discardableResult + func startSpan() -> Span { + return NOPOTelSpan() + } + + @discardableResult + func setParent(_ parent: Span) -> Self { + return self + } + + @discardableResult + func setParent(_ parent: SpanContext) -> Self { + return self + } + + @discardableResult + func setNoParent() -> Self { + return self + } + + @discardableResult + func addLink(spanContext: SpanContext) -> Self { + return self + } + + @discardableResult + func addLink(spanContext: SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { + return self + } + + @discardableResult + func setSpanKind(spanKind: SpanKind) -> Self { + return self + } + + @discardableResult + func setStartTime(time: Date) -> Self { + return self + } + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { + return self + } + + func setActive(_ active: Bool) -> Self { + return self + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelAttributeValue+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelAttributeValue+Datadog.swift new file mode 100644 index 0000000000..43844c65c9 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelAttributeValue+Datadog.swift @@ -0,0 +1,73 @@ +/* +* 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 OpenTelemetryApi + +extension Dictionary where Key == String, Value == OpenTelemetryApi.AttributeValue { + /// Converts OpenTelemetry attributes to Datadog tags. This method is recursive + /// and will flatten nested attributes. Collection attributes are flattened to multiple + /// tags with `key.index` naming convention. If attribute value is an empty collection, + /// it will be converted to empty string. + var tags: [String: String] { + var tags: [String: String] = [:] + for (key, value) in self { + switch value { + case .bool(let value): + tags[key] = value.description + case .string(let value): + tags[key] = value + case .int(let value): + tags[key] = value.description + case .double(let value): + tags[key] = value.description + case .stringArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element + } + } + case .boolArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element.description + } + } + case .intArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element.description + } + } + case .doubleArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element.description + } + } + case .set(let set): + if set.labels.tags.isEmpty { + tags[key] = "" + } else { + for (nestedKey, nestedValue) in set.labels.tags { + tags["\(key).\(nestedKey)"] = nestedValue + } + } + @unknown default: + break + } + } + return tags + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift new file mode 100644 index 0000000000..c8dd3292fa --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift @@ -0,0 +1,212 @@ +/* +* 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 +import OpenTelemetryApi + +internal enum DatadogTagKeys: String { + case spanKind = "span.kind" + case errorType = "error.type" + case errorMessage = "error.message" + case spanLinks = "_dd.span_links" +} + +extension OpenTelemetryApi.TraceId { + /// Returns 32 character hexadecimal string representation of lower 64 bits of the trace ID. + var lowerLongHexString: String { + return String(format: "%016llx", rawLowerLong) + } +} + +internal extension OpenTelemetryApi.Status { + /// Ok > Error > Unset + /// https://opentelemetry.io/docs/specs/otel/trace/api/#set-status + var priority: UInt { + switch self { + case .ok: + return 3 + case .error: + return 2 + case .unset: + return 1 + @unknown default: + return 1 + } + } +} + +internal class OTelSpan: OpenTelemetryApi.Span { + @ReadWriteLock + private var _status: OpenTelemetryApi.Status + + @ReadWriteLock + private var _name: String + + @ReadWriteLock + var attributes: [String: OpenTelemetryApi.AttributeValue] + let context: OpenTelemetryApi.SpanContext + @ReadWriteLock + var kind: OpenTelemetryApi.SpanKind + let ddSpan: DDSpan + let tracer: DatadogTracer + let spanLinks: [OTelSpanLink] + + /// `isRecording` indicates whether the span is recording or not + /// and events can be added to it. + @ReadWriteLock + var isRecording: Bool + + /// Saves status of the span indicating whether the span has recorded errors. + /// This will be done by setting `error.message` tag on the span. + var status: OpenTelemetryApi.Status { + get { + _status + } + set { + __status.mutate { + guard isRecording else { + return + } + + // If the code has been set to a higher value before (Ok > Error > Unset), + // the code will not be changed. + guard newValue.priority >= $0.priority else { + return + } + + $0 = newValue + } + } + } + + /// `name` of the span is akin to operation name in Datadog + var name: String { + get { + _name + } + set { + __name.mutate { + guard isRecording else { + return + } + $0 = newValue + ddSpan.setOperationName($0) + } + } + } + + init( + attributes: [String: OpenTelemetryApi.AttributeValue], + kind: OpenTelemetryApi.SpanKind, + name: String, + parentSpanID: OpenTelemetryApi.SpanId?, + spanContext: OpenTelemetryApi.SpanContext, + spanKind: OpenTelemetryApi.SpanKind, + spanLinks: [OTelSpanLink], + startTime: Date, + tracer: DatadogTracer, + eventBuilder: SpanEventBuilder, + eventWriter: SpanWriteContext + ) { + self._name = name + self._status = .unset + self.attributes = attributes + self.context = spanContext + self.kind = kind + self.isRecording = true + self.tracer = tracer + self.spanLinks = spanLinks + self.ddSpan = .init( + tracer: tracer, + context: .init( + traceID: context.traceId.toDatadog(), + spanID: context.spanId.toDatadog(), + parentSpanID: parentSpanID?.toDatadog(), + baggageItems: .init(), + sampleRate: tracer.localTraceSampler.samplingRate, + isKept: tracer.localTraceSampler.sample() + ), + operationName: name, + startTime: startTime, + tags: [:], + eventBuilder: eventBuilder, + eventWriter: eventWriter + ) + } + + func addEvent(name: String) { + DD.logger.warn("\(#function) is not yet supported in `DatadogTrace`") + } + + func addEvent(name: String, timestamp: Date) { + DD.logger.warn("\(#function) is not yet supported in `DatadogTrace`") + } + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue]) { + DD.logger.warn("\(#function) is not yet supported in `DatadogTrace`") + } + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue], timestamp: Date) { + DD.logger.warn("\(#function) is not yet supported in `DatadogTrace`") + } + + func end() { + end(time: Date()) + } + + func end(time: Date) { + var tags: [String: String] = [:] + + guard isRecording else { + return + } + isRecording = false + tags = attributes.tags + + // There is no need to lock here, because `DDSpan` is thread-safe + for (key, value) in tags { + ddSpan.setTag(key: key, value: value) + } + + switch status { + case .ok, .unset: + break + case .error(description: let description): + // set error tags on the span + tags[DatadogTagKeys.errorMessage.rawValue] = description + + // send error log to Datadog + // Empty kind or description is equivalent to not present + ddSpan.setError(kind: "", message: description) + @unknown default: + break + } + + // SpanKind maps to the `span.kind` tag in Datadog + ddSpan.setTag(key: DatadogTagKeys.spanKind.rawValue, value: kind.rawValue) + + // Datadog uses `_dd.span_links` tag to send span links + if !spanLinks.isEmpty { + ddSpan.setTag(key: DatadogTagKeys.spanLinks.rawValue, value: spanLinks) + } + + ddSpan.finish(at: time) + OpenTelemetry.instance.contextProvider.removeContextForSpan(self) + } + + var description: String { + return "OTelSpan" + } + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue?) { + guard isRecording else { + return + } + + attributes[key] = value + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift new file mode 100644 index 0000000000..c1746cb59f --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift @@ -0,0 +1,147 @@ +/* + * 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 OpenTelemetryApi + +internal class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder { + var tracer: DatadogTracer + var spanName: String + var spanKind = SpanKind.client + var attributes: [String: OpenTelemetryApi.AttributeValue] + var startTime: Date? + var active: Bool + var parent: Parent + var spanLinks: [OTelSpanLink] = [] + + enum Parent { + case currentSpan + case span(OpenTelemetryApi.Span) + case spanContext(OpenTelemetryApi.SpanContext) + case noParent + + func context() -> OpenTelemetryApi.SpanContext? { + switch self { + case .currentSpan: + return OpenTelemetry.instance.contextProvider.activeSpan?.context + case .span(let span): + return span.context + case .spanContext(let context): + return context + case .noParent: + return nil + } + } + } + + init( + active: Bool, + attributes: [String: OpenTelemetryApi.AttributeValue], + parent: Parent, + spanKind: SpanKind, + spanName: String, + startTime: Date?, + tracer: DatadogTracer + ) { + self.tracer = tracer + self.spanName = spanName + self.spanKind = spanKind + self.attributes = attributes + self.startTime = startTime + self.active = active + self.parent = parent + } + + func setParent(_ parent: OpenTelemetryApi.Span) -> Self { + self.parent = .span(parent) + return self + } + + func setParent(_ parent: OpenTelemetryApi.SpanContext) -> Self { + self.parent = .spanContext(parent) + return self + } + + func setNoParent() -> Self { + self.parent = .noParent + return self + } + + func addLink(spanContext: OpenTelemetryApi.SpanContext) -> Self { + self.spanLinks.append(OTelSpanLink(context: spanContext, attributes: [:])) + return self + } + + func addLink(spanContext: OpenTelemetryApi.SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { + self.spanLinks.append(OTelSpanLink(context: spanContext, attributes: attributes)) + return self + } + + func setSpanKind(spanKind: OpenTelemetryApi.SpanKind) -> Self { + self.spanKind = spanKind + return self + } + + func setStartTime(time: Date) -> Self { + self.startTime = time + return self + } + + func setActive(_ active: Bool) -> Self { + self.active = active + return self + } + + func startSpan() -> OpenTelemetryApi.Span { + let parentContext = parent.context() + let traceId: TraceId + let spanId = SpanId.random() + let traceState: TraceState + + if let parentContext = parentContext, parentContext.isValid { + traceId = parentContext.traceId + traceState = parentContext.traceState + } else { + traceId = TraceId.random() + traceState = .init() + } + + let spanContext = SpanContext.create( + traceId: traceId, + spanId: spanId, + traceFlags: TraceFlags(), + traceState: traceState + ) + + let writer = LazySpanWriteContext(featureScope: tracer.featureScope) + + let createdSpan = OTelSpan( + attributes: attributes, + kind: spanKind, + name: spanName, + parentSpanID: parentContext?.spanId, + spanContext: spanContext, + spanKind: spanKind, + spanLinks: spanLinks, + startTime: startTime ?? Date(), + tracer: tracer, + eventBuilder: tracer.spanEventBuilder, + eventWriter: writer + ) + + if active { + OpenTelemetry.instance.contextProvider.setActiveSpan(createdSpan) + createdSpan.ddSpan.setActive() + } + + return createdSpan + } + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { + attributes[key] = value + return self + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpanId+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpanId+Datadog.swift new file mode 100644 index 0000000000..6c8ff2c0b8 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpanId+Datadog.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 Foundation +import OpenTelemetryApi +import DatadogInternal + +extension OpenTelemetryApi.SpanId { + /// Converts OpenTelemetry `SpanId` to Datadog `SpanID`. + /// - Returns: Datadog `SpanID`. + func toDatadog() -> SpanID { + var data = Data(count: 8) + self.copyBytesTo(dest: &data, destOffset: 0) + let integerLiteral = UInt64(bigEndian: data.withUnsafeBytes { $0.load(as: UInt64.self) }) + return .init(integerLiteral: integerLiteral) + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpanLink.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpanLink.swift new file mode 100644 index 0000000000..04cb573126 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpanLink.swift @@ -0,0 +1,55 @@ +/* +* 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 OpenTelemetryApi + +/// Represents a span link containing a `SpanContext` and additional attributes. +internal struct OTelSpanLink: Equatable { + /// Context of the linked span. + let context: OpenTelemetryApi.SpanContext + + /// Additional attributes of the linked span. + let attributes: [String: OpenTelemetryApi.AttributeValue] +} + +extension OTelSpanLink: Encodable { + enum CodingKeys: String, CodingKey { + case traceId = "trace_id" + case spanId = "span_id" + case attributes = "attributes" + case traceState = "tracestate" + case traceFlags = "flags" + } + + /// Encodes the span link to the following JSON format: + /// ```json + /// { + /// "trace_id": "", + /// "span_id": "", + /// "attributes": {"key":"value", "pairs":"of", "arbitrary":"values"}, + /// "dropped_attributes_count": , + /// "tracestate": "a tracestate as defined in the W3C standard", + /// "flags": + /// }, + /// ``` + /// - Parameter encoder: Encoder + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let traceId = String(context.traceId.toDatadog(), representation: .hexadecimal32Chars) + + try container.encode(traceId, forKey: .traceId) + try container.encode(context.spanId.hexString, forKey: .spanId) + if !attributes.isEmpty { + try container.encode(attributes.tags, forKey: .attributes) + } + + if !context.traceState.entries.isEmpty { + try container.encode(context.traceState.w3c(), forKey: .traceState) + } + try container.encode(context.traceFlags.byte, forKey: .traceFlags) + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelTraceId+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelTraceId+Datadog.swift new file mode 100644 index 0000000000..4034fbfa9a --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelTraceId+Datadog.swift @@ -0,0 +1,17 @@ +/* +* 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 OpenTelemetryApi +import DatadogInternal + +extension OpenTelemetryApi.TraceId { + /// Converts OpenTelemetry `TraceId` to Datadog `TraceID`. + /// - Returns: Datadog `TraceID` with only higher order bits considered. + func toDatadog() -> TraceID { + return .init(idHi: self.idHi, idLo: self.idLo) + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelTraceState+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelTraceState+Datadog.swift new file mode 100644 index 0000000000..bac0d2d8ff --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelTraceState+Datadog.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 Foundation +import OpenTelemetryApi + +extension OpenTelemetryApi.TraceState { + /// Returns the tracestate as a string as defined in the W3C standard. + /// https://www.w3.org/TR/trace-context/#tracestate-header-field-values + /// Example: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE + /// - Returns: tracestate as a string + public func w3c() -> String { + return self.entries.map { "\($0.key)=\($0.value)" }.joined(separator: ",") + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelTracerProvider.swift b/DatadogTrace/Sources/OpenTelemetry/OTelTracerProvider.swift new file mode 100644 index 0000000000..303b405cd6 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelTracerProvider.swift @@ -0,0 +1,73 @@ +/* +* 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 +import OpenTelemetryApi + +/// The Datadog implementation of OpenTelemetry `TracerProvider`. +/// It takes the Datadog SDK instance as a dependency and returns the tracer from it. +/// +/// Usage: +/// +/// ```swift +/// import OpenTelemetryApi +/// import DatadogTrace +/// +/// // Register the tracer provider +/// OpenTelemetry.registerTracerProvider( +/// tracerProvider: OTelTracerProvider() +/// ) +/// +/// // Get the tracer +/// let tracer = OpenTelemetry +/// .instance +/// .tracerProvider +/// .get(instrumentationName: "", instrumentationVersion: nil) +/// +/// // Start a span +/// let span = tracer +/// .spanBuilder(spanName: "OperationName") +/// .startSpan() +/// ``` +public class OTelTracerProvider: OpenTelemetryApi.TracerProvider { + private weak var core: DatadogCoreProtocol? + + /// Creates a tracer provider with the given Datadog SDK instance. + /// - Parameter core: the instance of Datadog SDK the Trace feature was enabled in (global instance by default) + public init(in core: DatadogCoreProtocol = CoreRegistry.default) { + self.core = core + } + + /// Returns a tracer with the given instrumentation name and version. + /// - Parameters: + /// - instrumentationName: the name of the instrumentation library, not the name of the instrumented library + /// Note: This is ignored, as the Datadog SDK works on concept of core. + /// - instrumentationVersion: The version of the instrumentation library (e.g., "semver:1.0.0"). Optional + /// Note: This is ignored, as the Datadog SDK works on concept of core. + public func get(instrumentationName: String, instrumentationVersion: String?) -> OpenTelemetryApi.Tracer { + do { + guard !(core is NOPDatadogCore) else { + throw ProgrammerError( + description: "Datadog SDK must be initialized and Trace feature must be enabled before calling `OTelTracerProvider.get(instrumentationName:instrumentationVersion:)`." + ) + } + guard let feature = core?.get(feature: TraceFeature.self) else { + throw ProgrammerError( + description: "Trace feature must be enabled before calling `OTelTracerProvider.get(instrumentationName:instrumentationVersion:)`." + ) + } + + // Send tracer API usage to telemetry + core?.telemetry.configuration(tracerAPI: "OpenTelemetry", tracerAPIVersion: OpenTelemetry.version) + + return feature.tracer + } catch { + consolePrint("\(error)", .error) + return DDNoopTracer() + } + } +} diff --git a/DatadogTrace/Sources/Span/SpanEventBuilder.swift b/DatadogTrace/Sources/Span/SpanEventBuilder.swift index 7405400bb6..ac72bb9877 100644 --- a/DatadogTrace/Sources/Span/SpanEventBuilder.swift +++ b/DatadogTrace/Sources/Span/SpanEventBuilder.swift @@ -75,8 +75,8 @@ internal struct SpanEventBuilder { traceID: traceID, spanID: spanID, parentID: parentSpanID, - operationName: operationName, - serviceName: service ?? context.service, + operationName: tagsReducer.extractedOperationName ?? operationName, + serviceName: tagsReducer.extractedServiceName ?? service ?? context.service, resource: tagsReducer.extractedResourceName ?? operationName, startTime: startTime.addingTimeInterval(context.serverTimeOffset), duration: finishTime.timeIntervalSince(startTime), diff --git a/DatadogTrace/Sources/Span/SpanTagsReducer.swift b/DatadogTrace/Sources/Span/SpanTagsReducer.swift index d706f80c66..69363df2cf 100644 --- a/DatadogTrace/Sources/Span/SpanTagsReducer.swift +++ b/DatadogTrace/Sources/Span/SpanTagsReducer.swift @@ -26,6 +26,10 @@ internal struct SpanTagsReducer { let extractedIsError: Bool? /// Resource name requiring a special encoding in `Span` JSON. let extractedResourceName: String? + /// Extracted operation name from operation tag. + let extractedOperationName: String? + /// Extracted service name from service tag. + let extractedServiceName: String? // MARK: - Initialization @@ -34,6 +38,8 @@ internal struct SpanTagsReducer { var extractedIsError: Bool? = nil var extractedResourceName: String? = nil + var extractedOperationName: String? = nil + var extractedServiceName: String? = nil // extract error from `logFields` for fields in logFields { @@ -59,8 +65,18 @@ internal struct SpanTagsReducer { extractedResourceName = resourceName } + if let operationName = mutableSpanTags.removeValue(forKey: SpanTags.operation) as? String { + extractedOperationName = operationName + } + + if let serviceName = mutableSpanTags.removeValue(forKey: SpanTags.service) as? String { + extractedServiceName = serviceName + } + self.reducedSpanTags = mutableSpanTags self.extractedIsError = extractedIsError self.extractedResourceName = extractedResourceName + self.extractedOperationName = extractedOperationName + self.extractedServiceName = extractedServiceName } } diff --git a/DatadogTrace/Sources/Trace.swift b/DatadogTrace/Sources/Trace.swift index e523314b50..24717180ae 100644 --- a/DatadogTrace/Sources/Trace.swift +++ b/DatadogTrace/Sources/Trace.swift @@ -43,21 +43,25 @@ public enum Trace { if let firstPartyHostsTracing = configuration.urlSessionTracking?.firstPartyHostsTracing { let distributedTraceSampler: Sampler let firstPartyHosts: FirstPartyHosts + let traceContextInjection: TraceContextInjection switch firstPartyHostsTracing { - case let .trace(hosts, sampleRate): + case let .trace(hosts, sampleRate, injection): distributedTraceSampler = Sampler(samplingRate: configuration.debugSDK ? 100 : sampleRate) firstPartyHosts = FirstPartyHosts(hosts) - case let .traceWithHeaders(hostsWithHeaders, sampleRate): + traceContextInjection = injection + case let .traceWithHeaders(hostsWithHeaders, sampleRate, injection): distributedTraceSampler = Sampler(samplingRate: configuration.debugSDK ? 100 : sampleRate) firstPartyHosts = FirstPartyHosts(hostsWithHeaders) + traceContextInjection = injection } let urlSessionHandler = TracingURLSessionHandler( tracer: trace.tracer, contextReceiver: trace.contextReceiver, distributedTraceSampler: distributedTraceSampler, - firstPartyHosts: firstPartyHosts + firstPartyHosts: firstPartyHosts, + traceContextInjection: traceContextInjection ) try core.register(urlSessionHandler: urlSessionHandler) diff --git a/DatadogTrace/Sources/TraceConfiguration.swift b/DatadogTrace/Sources/TraceConfiguration.swift index 0de05d1825..abb12e66e2 100644 --- a/DatadogTrace/Sources/TraceConfiguration.swift +++ b/DatadogTrace/Sources/TraceConfiguration.swift @@ -18,6 +18,7 @@ import DatadogInternal @_exported import class DatadogInternal.B3HTTPHeadersWriter @_exported import class DatadogInternal.W3CHTTPHeadersWriter @_exported import enum DatadogInternal.TraceSamplingStrategy +@_exported import enum DatadogInternal.TraceContextInjection // swiftlint:enable duplicate_imports extension Trace { @@ -104,13 +105,23 @@ extension Trace { /// - Parameters: /// - hosts: The set of hosts to inject tracing headers. Note: Hosts must not include the "http(s)://" prefix. /// - sampleRate: The sampling rate for tracing. Must be a value between `0.0` and `100.0`. Default: `20`. - case trace(hosts: Set, sampleRate: Float = 20) + /// - traceControlInjection: The strategy for injecting trace context into requests. Default: `.all`. + case trace( + hosts: Set, + sampleRate: Float = 20, + traceControlInjection: TraceContextInjection = .all + ) /// Trace given hosts with using custom tracing headers. /// /// - `hostsWithHeaders` - Dictionary of hosts and tracing header types to use. Note: Hosts must not include "http(s)://" prefix. /// - `sampleRate` - The sampling rate for tracing. Must be a value between `0.0` and `100.0`. Default: `20`. - case traceWithHeaders(hostsWithHeaders: [String: Set], sampleRate: Float = 20) + /// - traceControlInjection: The strategy for injecting trace context into requests. Default: `.all`. + case traceWithHeaders( + hostsWithHeaders: [String: Set], + sampleRate: Float = 20, + traceControlInjection: TraceContextInjection = .all + ) } /// Configuration for automatic network requests tracing. diff --git a/DatadogTrace/Sources/Tracer.swift b/DatadogTrace/Sources/Tracer.swift index 2afaf83036..05147d18f0 100644 --- a/DatadogTrace/Sources/Tracer.swift +++ b/DatadogTrace/Sources/Tracer.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import OpenTelemetryApi /// Datadog - specific span tags to be used with `Tracer.shared().startSpan(operationName:references:tags:startTime:)` /// and `span.setTag(key:value:)`. @@ -16,6 +17,10 @@ public enum SpanTags { /// /// Expects `String` value set for a tag. public static let resource = "resource.name" + /// A Datadog-specific span tag, which sets the operation name + public static let operation = "operation.name" + /// A Datadog-specific span tag, which sets the value appearing in the "SERVICE" column + public static let service = "service.name" /// Internal tag. `Integer` value. Measures elapsed time at app's foreground state in nanoseconds. /// (duration - foregroundDuration) gives you the elapsed time while the app wasn't active (probably at background) internal static let foregroundDuration = "foreground_duration" @@ -71,6 +76,9 @@ public class Tracer { ) } + // Send tracer API usage to telemetry + core.telemetry.configuration(tracerAPI: "OpenTracing") + return feature.tracer } catch { consolePrint("\(error)", .error) diff --git a/DatadogTrace/Tests/DDNoopTracerTests.swift b/DatadogTrace/Tests/DDNoopTracerTests.swift index ecea81ad79..a2ed90e489 100644 --- a/DatadogTrace/Tests/DDNoopTracerTests.swift +++ b/DatadogTrace/Tests/DDNoopTracerTests.swift @@ -20,7 +20,10 @@ class DDNoopTracerTests: XCTestCase { // When let context = DDSpanContext.mockAny() - noop.inject(spanContext: context, writer: HTTPHeadersWriter(samplingStrategy: .headBased)) + noop.inject( + spanContext: context, + writer: HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + ) _ = 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/OpenTelemetry/OTelAttributeValue+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelAttributeValue+DatadogTests.swift new file mode 100644 index 0000000000..7a0d69fffb --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelAttributeValue+DatadogTests.swift @@ -0,0 +1,131 @@ +/* + * 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 +import OpenTelemetryApi + +@testable import DatadogTrace + +final class OTelAttributeValueDatadogTests: XCTestCase { + func testTags_givenMultipleLevelsAttributes() { + // Given + let attributes = makeAttributes(level: 3) + + // When + let tags = attributes.tags + + let expectedTags = + [ + "key3-0": "true", + "key3-1": "value1", + "key3-2": "2", + "key3-3": "3.0", + "key3-4.0": "value4", + "key3-4.1": "value5", + "key3-5.0": "true", + "key3-5.1": "false", + "key3-6.0": "7", + "key3-6.1": "8", + "key3-7.0": "7.0", + "key3-7.1": "8.0", + "key3-8.key2-0": "true", + "key3-8.key2-1": "value1", + "key3-8.key2-2": "2", + "key3-8.key2-3": "3.0", + "key3-8.key2-4.0": "value4", + "key3-8.key2-4.1": "value5", + "key3-8.key2-5.0": "true", + "key3-8.key2-5.1": "false", + "key3-8.key2-6.0": "7", + "key3-8.key2-6.1": "8", + "key3-8.key2-7.0": "7.0", + "key3-8.key2-7.1": "8.0", + "key3-8.key2-8.key1-0": "true", + "key3-8.key2-8.key1-1": "value1", + "key3-8.key2-8.key1-2": "2", + "key3-8.key2-8.key1-3": "3.0", + "key3-8.key2-8.key1-4.0": "value4", + "key3-8.key2-8.key1-4.1": "value5", + "key3-8.key2-8.key1-5.0": "true", + "key3-8.key2-8.key1-5.1": "false", + "key3-8.key2-8.key1-6.0": "7", + "key3-8.key2-8.key1-6.1": "8", + "key3-8.key2-8.key1-7.0": "7.0", + "key3-8.key2-8.key1-7.1": "8.0", + "key3-8.key2-8.key1-8": "" // when recursion ends, empty string is returned + ] + + // Then + DDAssertDictionariesEqual(expectedTags, tags) + } + + func testTags_givenOneLevelAttributesWithEmptyCollections() { + // Given + let attributes: [String: OpenTelemetryApi.AttributeValue] = [ + "key1": .bool(true), + "key2": .string("value1"), + "key3": .int(2), + "key4": .double(3.0), + "key5": .stringArray(["value5", "value6"]), + "key6": .boolArray([true, false]), + "key7": .intArray([7, 8]), + "key8": .doubleArray([8.0, 9.0]), + "key9": .set(.init(labels: [:])), + "key10": .stringArray([]), + "key11": .boolArray([]), + "key12": .intArray([]), + "key13": .doubleArray([]), + ] + + // When + let tags = attributes.tags + + // Then + let expectedTags = + [ + "key1": "true", + "key2": "value1", + "key3": "2", + "key4": "3.0", + "key5.0": "value5", + "key5.1": "value6", + "key6.0": "true", + "key6.1": "false", + "key7.0": "7", + "key7.1": "8", + "key8.0": "8.0", + "key8.1": "9.0", + "key9": "", + "key10": "", + "key11": "", + "key12": "", + "key13": "", + ] + DDAssertDictionariesEqual(expectedTags, tags) + } + + // MARK: - Helpers + + func makeAttributes(level: UInt) -> [String: OpenTelemetryApi.AttributeValue] { + guard level > 0 else { + return [:] + } + + return [ + "key\(level)-0": .bool(true), + "key\(level)-1": .string("value1"), + "key\(level)-2": .int(2), + "key\(level)-3": .double(3.0), + "key\(level)-4": .stringArray(["value4", "value5"]), + "key\(level)-5": .boolArray([true, false]), + "key\(level)-6": .intArray([7, 8]), + "key\(level)-7": .doubleArray([7.0, 8.0]), + "key\(level)-8": .set(.init(labels: makeAttributes(level: level - 1))) + ] + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelSpanId+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelSpanId+DatadogTests.swift new file mode 100644 index 0000000000..8085592062 --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelSpanId+DatadogTests.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 XCTest +import TestUtilities +import DatadogInternal +import OpenTelemetryApi + +@testable import DatadogTrace + +class OTelSpanIdDatadogTests: XCTestCase { + func testToDatadog() { + let otelId = SpanId.random() + let ddId = otelId.toDatadog() + XCTAssertEqual(otelId.rawValue, ddId.rawValue) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelSpanLinkTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelSpanLinkTests.swift new file mode 100644 index 0000000000..8409016197 --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelSpanLinkTests.swift @@ -0,0 +1,81 @@ +/* + * 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 +import OpenTelemetryApi + +@testable import DatadogTrace + +final class OTelSpanLinkTests: XCTestCase { + func testEncoder_givenAllPropertiesArePresent() throws { + let encoder = JSONEncoder() + let traceId = TraceId(idHi: 101, idLo: 102) + let spanId = SpanId(id: 103) + var traceFlags = TraceFlags() + traceFlags.setIsSampled(true) + let traceState = TraceState( + entries: [ + .init(key: "foo", value: "bar")!, + .init(key: "bar", value: "baz")! + ] + )! + + let spanContext = OpenTelemetryApi.SpanContext.create( + traceId: traceId, + spanId: spanId, + traceFlags: traceFlags, + traceState: traceState + ) + let attributes: [String: OpenTelemetryApi.AttributeValue] = [ + "foo": .string("bar") + ] + + let spanLink = OTelSpanLink( + context: spanContext, + attributes: attributes + ) + + let encoded = try encoder.encode(spanLink) + let decoded = try JSONDecoder().decode([String: AnyDecodable].self, from: encoded) + + XCTAssertEqual(decoded["trace_id"]?.value as? String, "00000000000000650000000000000066") + XCTAssertEqual(decoded["span_id"]?.value as? String, "0000000000000067") + XCTAssertEqual(decoded["attributes"]?.value as? [String: String], ["foo": "bar"]) + XCTAssertEqual(decoded["tracestate"]?.value as? String, "foo=bar,bar=baz") + XCTAssertEqual(decoded["flags"]?.value as? Int, 1) + } + + func testEncoder_givenOnlyRequiredPropertiesArePresent() throws { + let encoder = JSONEncoder() + let traceId = TraceId(idHi: 101, idLo: 102) + let spanId = SpanId(id: 103) + let traceFlags = TraceFlags() + let traceState = TraceState() + + let spanContext = OpenTelemetryApi.SpanContext.create( + traceId: traceId, + spanId: spanId, + traceFlags: traceFlags, + traceState: traceState + ) + + let spanLink = OTelSpanLink( + context: spanContext, + attributes: [:] + ) + + let encoded = try encoder.encode(spanLink) + let decoded = try JSONDecoder().decode([String: AnyDecodable].self, from: encoded) + + XCTAssertEqual(decoded["trace_id"]?.value as? String, "00000000000000650000000000000066") + XCTAssertEqual(decoded["span_id"]?.value as? String, "0000000000000067") + XCTAssertNil(decoded["attributes"]?.value) + XCTAssertNil(decoded["tracestate"]?.value) + XCTAssertEqual(decoded["flags"]?.value as? Int, 0) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelSpanTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelSpanTests.swift new file mode 100644 index 0000000000..cc70869a47 --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelSpanTests.swift @@ -0,0 +1,462 @@ +/* + * 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 + +final class OTelSpanTests: XCTestCase { + func testSpanResourceNameDefault() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.resource, "OperationName") + XCTAssertEqual(recordedSpan.operationName, "OperationName") + } + + func testSpanOperationNameAttribute() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "https://httpbin.org/get").startSpan() + + // When + span.setAttribute(key: "operation.name", value: .string("GET")) + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.resource, "https://httpbin.org/get") + XCTAssertEqual(recordedSpan.operationName, "GET") + } + + func testSpanServiceNameDefault() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.serviceName, "abc") + } + + func testSpanServiceNameAttribute() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.setAttribute(key: "service.name", value: .string("ServiceName")) + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.serviceName, "ServiceName") + } + + func testSpanResourceNameAttribute() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.setAttribute(key: "resource.name", value: "ResourceName") + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.resource, "ResourceName") + XCTAssertEqual(recordedSpan.operationName, "OperationName") + } + + func testSpanSetName() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.name = "NewOperationName" + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.resource, "NewOperationName") + XCTAssertEqual(recordedSpan.operationName, "NewOperationName") + } + + func testSpanEnd() { + // Given + let (name, ignoredName) = ("trueName", "invalidName") + let (attributes, ignoredAttributes) = (["key": "value"], ["ignoredKey": "ignoredValue"]) + + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: name).startSpan() + for (key, value) in attributes { + span.setAttribute(key: key, value: value) + } + XCTAssertTrue(span.isRecording) + + // When + span.end() + XCTAssertFalse(span.isRecording) + + // Then ignores + span.name = ignoredName + for (key, value) in ignoredAttributes { + span.setAttribute(key: key, value: value) + } + + span.end() + + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + + XCTAssertEqual(recordedSpan.resource, name) + XCTAssertEqual(recordedSpan.operationName, name) + let expectedTags = [ + "key": "value", + "span.kind": "internal", + ] + DDAssertDictionariesEqual(recordedSpan.tags, expectedTags) + } + + func testSetParentSpan() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").startSpan() + _ = tracer.spanBuilder(spanName: "Noise").startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").setParent(parentSpan).startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertNil(parent.parentID) + XCTAssertEqual(child.parentID, parent.spanID) + } + + func testSetParentContext() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").startSpan() + _ = tracer.spanBuilder(spanName: "Noise").startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").setParent(parentSpan.context).startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertNil(parent.parentID) + XCTAssertEqual(child.parentID, parent.spanID) + } + + func testSetNoParent() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").startSpan() + _ = tracer.spanBuilder(spanName: "Noise").startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").setNoParent().startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertNil(parent.parentID) + XCTAssertEqual(child.parentID, nil) + } + + func testSetActive_givenParentSpan() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").setActive(true).startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertEqual(child.traceID, parent.traceID) + XCTAssertNil(parent.parentID) + XCTAssertEqual(child.parentID, parent.spanID) + } + + func testParentIds_givenDisjointSpans() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span1 = tracer.spanBuilder(spanName: "Span1").startSpan() + let span2 = tracer.spanBuilder(spanName: "Span2").startSpan() + + // When + span2.end() + span1.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let span1Recorded = recordedSpans.first! + let span2Recorded = recordedSpans.last! + + XCTAssertEqual(span1Recorded.parentID, nil) + XCTAssertEqual(span2Recorded.parentID, nil) + XCTAssertNotEqual(span1Recorded.traceID, span2Recorded.traceID) + } + + func testSetAttribute() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + + // When + span.setAttribute(key: "key", value: .bool(true)) + span.setAttribute(key: "key2", value: .string("value2")) + span.setAttribute(key: "key3", value: .int(3)) + span.setAttribute(key: "key4", value: .double(4.0)) + + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + let expectedTags = + [ + "key": "true", + "key2": "value2", + "key3": "3", + "key4": "4.0", + "span.kind": "internal", + ] + DDAssertDictionariesEqual(recordedSpan.tags, expectedTags) + } + + func testStatus_whenStatusIsNotSet() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + + // When + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + + let recordedSpan = recordedSpans.first! + XCTAssertFalse(recordedSpan.isError) + XCTAssertEqual(recordedSpan.tags["error.type"], nil) + XCTAssertEqual(recordedSpan.tags["error.message"], nil) + XCTAssertEqual(recordedSpan.tags["error.stack"], nil) + } + + func testStatus_whenStatusIsOk() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + + // When + span.status = .ok + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + + let recordedSpan = recordedSpans.first! + XCTAssertFalse(recordedSpan.isError) + XCTAssertEqual(recordedSpan.tags["error.type"], nil) + XCTAssertEqual(recordedSpan.tags["error.message"], nil) + } + + func testStatus_whenStatusIsErrorWithMessage() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + + // When + span.status = .error(description: "error description") + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + + let recordedSpan = recordedSpans.first! + XCTAssertTrue(recordedSpan.isError) + XCTAssertEqual(recordedSpan.tags["error.type"], "") + // In OLTP world, error message is set as `error.message` + // but during the migration we want to keep it as `error.msg`. + // https://github.com/open-telemetry/opentelemetry-proto/blob/724e427879e3d2bae2edc0218fff06e37b9eb46e/opentelemetry/proto/trace/v1/trace.proto#L264 + XCTAssertEqual(recordedSpan.tags["error.msg"], "error description") + } + + func testStatus_givenStatusOk_whenSetStatusCalledWithErrorAndUnset() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + span.status = .ok + + // When + span.status = .error(description: "error description") + span.status = .unset + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + + let recordedSpan = recordedSpans.first! + XCTAssertFalse(recordedSpan.isError) + XCTAssertEqual(recordedSpan.tags["error.type"], nil) + XCTAssertEqual(recordedSpan.tags["error.msg"], nil) + } + + func testStatus_givenStatusError_whenSetStatusCalledWithUnset() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + span.status = .error(description: "error description") + + // When + span.status = .unset + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + + let recordedSpan = recordedSpans.first! + XCTAssertTrue(recordedSpan.isError) + XCTAssertEqual(recordedSpan.tags["error.type"], "") + XCTAssertEqual(recordedSpan.tags["error.msg"], "error description") + } +} + +extension PassthroughCoreMock { + func spans() -> [SpanEvent] { + let events: [SpanEventsEnvelope] = self.events() + return events.flatMap { $0.spans } + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelTraceId+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelTraceId+DatadogTests.swift new file mode 100644 index 0000000000..29e04f896f --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelTraceId+DatadogTests.swift @@ -0,0 +1,21 @@ +/* + * 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 +import OpenTelemetryApi + +@testable import DatadogTrace + +class OTelTraceIdDatadogTests: XCTestCase { + func testToDatadog_onlyHigherOrderBitsAreConsidered() { + let otelId = TraceId.random() + let ddId = otelId.toDatadog() + XCTAssertEqual(otelId.idLo, ddId.idLo) + XCTAssertEqual(otelId.idHi, ddId.idHi) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelTraceState+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelTraceState+DatadogTests.swift new file mode 100644 index 0000000000..6cf0582b6e --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelTraceState+DatadogTests.swift @@ -0,0 +1,30 @@ +/* + * 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 +import OpenTelemetryApi + +@testable import DatadogTrace + +final class OTelTraceStateDatadogTests: XCTestCase { + func testW3C_givenEmptyEntries() throws { + let traceState = TraceState(entries: [])! + XCTAssertEqual("", traceState.w3c()) + } + + func testW3C_givenSomeEntries() throws { + let traceState = TraceState( + entries: [ + .init(key: "foo", value: "bar")!, + .init(key: "bar", value: "baz")! + ] + )! + + XCTAssertEqual("foo=bar,bar=baz", traceState.w3c()) + } +} diff --git a/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift b/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift index 52589f5ffa..e0ad6e459b 100644 --- a/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift +++ b/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift @@ -7,6 +7,7 @@ import XCTest import TestUtilities import DatadogInternal +import OpenTelemetryApi @testable import DatadogTrace @@ -279,6 +280,58 @@ class SpanEventBuilderTests: XCTestCase { XCTAssertEqual(span.tags, [:]) } + func testBuildingSpanWithOperationNameTagSet() { + let builder: SpanEventBuilder = .mockAny() + + // given + let span = builder.createSpanEvent( + context: .mockAny(), + traceID: .mockAny(), + spanID: .mockAny(), + parentSpanID: .mockAny(), + operationName: .mockAny(), + startTime: .mockAny(), + finishTime: .mockAny(), + samplingRate: .mockAny(), + isKept: .mockAny(), + tags: [ + SpanTags.operation: "custom operation name" + ], + baggageItems: [:], + logFields: [] + ) + + // then + XCTAssertEqual(span.operationName, "custom operation name") + XCTAssertEqual(span.tags, [:]) + } + + func testBuildingSpanWithServiceNameTagSet() { + let builder: SpanEventBuilder = .mockAny() + + // given + let span = builder.createSpanEvent( + context: .mockAny(), + traceID: .mockAny(), + spanID: .mockAny(), + parentSpanID: .mockAny(), + operationName: .mockAny(), + startTime: .mockAny(), + finishTime: .mockAny(), + samplingRate: .mockAny(), + isKept: .mockAny(), + tags: [ + SpanTags.service: "custom service name" + ], + baggageItems: [:], + logFields: [] + ) + + // then + XCTAssertEqual(span.serviceName, "custom service name") + XCTAssertEqual(span.tags, [:]) + } + func testItSendsBaggageItemsAsTags() { let builder: SpanEventBuilder = .mockAny() @@ -500,6 +553,64 @@ class SpanEventBuilderTests: XCTestCase { XCTAssertEqual(dd.logger.errorLog?.error?.message, "Value cannot be encoded.") } + func testBuildingSpanWhenSpanLinkIsPresentInTags() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let builder: SpanEventBuilder = .mockAny() + + let traceId = TraceId(idHi: 101, idLo: 102) + let spanId = SpanId(id: 103) + var traceFlags = TraceFlags() + traceFlags.setIsSampled(true) + let traceState = TraceState( + entries: [ + .init(key: "foo", value: "bar")!, + .init(key: "bar", value: "baz")! + ] + )! + + let spanContext = OpenTelemetryApi.SpanContext.create( + traceId: traceId, + spanId: spanId, + traceFlags: traceFlags, + traceState: traceState + ) + let attributes: [String: OpenTelemetryApi.AttributeValue] = [ + "foo": .string("bar") + ] + + let spanLink = OTelSpanLink( + context: spanContext, + attributes: attributes + ) + + // When + let span = builder.createSpanEvent( + context: .mockAny(), + traceID: .mockAny(), + spanID: .mockAny(), + parentSpanID: .mockAny(), + operationName: .mockAny(), + startTime: .mockAny(), + finishTime: .mockAny(), + samplingRate: .mockAny(), + isKept: .mockAny(), + tags: [ + DatadogTagKeys.spanLinks.rawValue: [spanLink] + ], + baggageItems: [:], + logFields: [] + ) + + // Then + XCTAssertEqual(span.tags.count, 1) + let expectedTags = "[{\"attributes\":{\"foo\":\"bar\"},\"flags\":1,\"span_id\":\"0000000000000067\",\"trace_id\":\"00000000000000650000000000000066\",\"tracestate\":\"foo=bar,bar=baz\"}]" + let actualTags = span.tags["_dd.span_links"] + + DDAssertJSONEqual(expectedTags, actualTags!) + } + // MARK: - RUM context enrichment // swiftlint:disable opening_brace diff --git a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift index e52be47ae0..57353102fa 100644 --- a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift +++ b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift @@ -34,7 +34,8 @@ class TracingURLSessionHandlerTests: XCTestCase { distributedTraceSampler: .mockKeepAll(), firstPartyHosts: .init([ "www.example.com": [.datadog] - ]) + ]), + traceContextInjection: .all ) } @@ -49,7 +50,8 @@ class TracingURLSessionHandlerTests: XCTestCase { tracer: tracer, contextReceiver: ContextMessageReceiver(), distributedTraceSampler: .mockKeepAll(), - firstPartyHosts: .init() + firstPartyHosts: .init(), + traceContextInjection: .all ) // When @@ -89,7 +91,8 @@ class TracingURLSessionHandlerTests: XCTestCase { tracer: tracer, contextReceiver: ContextMessageReceiver(), distributedTraceSampler: .mockKeepAll(), - firstPartyHosts: .init() + firstPartyHosts: .init(), + traceContextInjection: .all ) // When @@ -137,7 +140,8 @@ class TracingURLSessionHandlerTests: XCTestCase { tracer: tracer, contextReceiver: ContextMessageReceiver(), distributedTraceSampler: .mockRejectAll(), - firstPartyHosts: .init() + firstPartyHosts: .init(), + traceContextInjection: .sampled ) // When @@ -153,13 +157,13 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField)) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField)) - XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField)) XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField)) XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField)) XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField)) - 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(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField)) + XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field)) + XCTAssertNil(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent)) XCTAssertNil(traceContext, "It must return no trace context") } @@ -170,7 +174,8 @@ class TracingURLSessionHandlerTests: XCTestCase { tracer: tracer, contextReceiver: ContextMessageReceiver(), distributedTraceSampler: .mockKeepAll(), - firstPartyHosts: .init() + firstPartyHosts: .init(), + traceContextInjection: .all ) let span = tracer.startRootSpan(operationName: "root") @@ -383,7 +388,8 @@ class TracingURLSessionHandlerTests: XCTestCase { tracer: .mockWith(core: core), contextReceiver: receiver, distributedTraceSampler: .mockKeepAll(), - firstPartyHosts: .init() + firstPartyHosts: .init(), + traceContextInjection: .all ) core.context.applicationStateHistory = .mockAppInForeground() diff --git a/DatadogWebViewTracking.podspec b/DatadogWebViewTracking.podspec index 72ad6990c5..4db4326cb6 100644 --- a/DatadogWebViewTracking.podspec +++ b/DatadogWebViewTracking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogWebViewTracking" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Datadog WebView Tracking Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift b/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift new file mode 100644 index 0000000000..ebaad783a7 --- /dev/null +++ b/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift @@ -0,0 +1,116 @@ +/* + * 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) +#warning("Datadog WebView Tracking does not support tvOS") +#else +import WebKit +#endif + +@objc(DDWebViewTracking) +@_spi(objc) +public final class objc_WebViewTracking: NSObject { + override private init() { } + + /// 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. + @objc(DDWebViewTrackingSessionReplayConfiguration) + @_spi(objc) + public final class SessionReplayConfiguration: NSObject { + /// Available privacy levels for content masking. + @objc(DDPrivacyLevel) + @_spi(objc) + public enum PrivacyLevel: Int { + /// Record all content. + case allow + /// Mask all content. + case mask + /// Mask input elements, but record all other content. + case maskUserInput + + internal var toSwift: WebViewTracking.SessionReplayConfiguration.PrivacyLevel { + switch self { + case .allow: return .allow + case .mask: return .mask + case .maskUserInput: return .maskUserInput + } + } + } + + /// The privacy level to use for the web view replay recording. + @objc public var privacyLevel: PrivacyLevel + + /// Creates Webview Session Replay configuration. + /// + /// - Parameters: + /// - privacyLevel: The way sensitive content (e.g. text) should be masked. Default: `.mask`. + @objc + override public init() { + self.privacyLevel = .mask + } + + /// Creates Webview Session Replay configuration. + /// + /// - Parameters: + /// - privacyLevel: The way sensitive content (e.g. text) should be masked. Default: `.mask`. + @objc + public init( + privacyLevel: PrivacyLevel + ) { + self.privacyLevel = privacyLevel + } + + internal var toSwift: WebViewTracking.SessionReplayConfiguration { + return .init( + privacyLevel: privacyLevel.toSwift + ) + } + } + + /// 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. + /// + /// - Parameters: + /// - webView: The web-view to track. + /// - 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. + @objc + public static func enable( + webView: WKWebView, + hosts: Set = [], + logsSampleRate: Float = 100, + with configuration: SessionReplayConfiguration? = nil + ) { + WebViewTracking.enable( + webView: webView, + hosts: hosts, + logsSampleRate: logsSampleRate, + sessionReplayConfiguration: configuration?.toSwift + ) + } + + /// Disables Datadog iOS SDK and Datadog Browser SDK integration. + /// + /// Removes Datadog's ScriptMessageHandler and UserScript from the caller. + /// - Note: This method **must** be called when the webview can be deinitialized. + /// + /// - Parameter webView: The web-view to stop tracking. + @objc + public static func disable( + webView: WKWebView + ) { + WebViewTracking.disable(webView: webView) + } +} diff --git a/E2ETests/E2ETests.xcodeproj/.xcodesamplecode.plist b/E2ETests/E2ETests.xcodeproj/.xcodesamplecode.plist new file mode 100644 index 0000000000..4bc741ca64 --- /dev/null +++ b/E2ETests/E2ETests.xcodeproj/.xcodesamplecode.plist @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/E2ETests/E2ETests.xcodeproj/project.pbxproj b/E2ETests/E2ETests.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6f840fc721 --- /dev/null +++ b/E2ETests/E2ETests.xcodeproj/project.pbxproj @@ -0,0 +1,548 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + D292E3BB2BD7BCDF0083453D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D292E3BA2BD7BCDF0083453D /* AppDelegate.swift */; }; + D2A6109B2BE11A74000AA6AB /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = D2A6109A2BE11A74000AA6AB /* DatadogCore */; }; + D2A6109D2BE11A74000AA6AB /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = D2A6109C2BE11A74000AA6AB /* DatadogCrashReporting */; }; + D2A6109F2BE11A74000AA6AB /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = D2A6109E2BE11A74000AA6AB /* DatadogLogs */; }; + D2A610A12BE11A74000AA6AB /* DatadogObjc in Frameworks */ = {isa = PBXBuildFile; productRef = D2A610A02BE11A74000AA6AB /* DatadogObjc */; }; + D2A610A32BE11A74000AA6AB /* DatadogRUM in Frameworks */ = {isa = PBXBuildFile; productRef = D2A610A22BE11A74000AA6AB /* DatadogRUM */; }; + D2A610A52BE11A74000AA6AB /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = D2A610A42BE11A74000AA6AB /* DatadogSessionReplay */; }; + D2A610A72BE11A74000AA6AB /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = D2A610A62BE11A74000AA6AB /* DatadogTrace */; }; + D2A610A92BE11A74000AA6AB /* DatadogWebViewTracking in Frameworks */ = {isa = PBXBuildFile; productRef = D2A610A82BE11A74000AA6AB /* DatadogWebViewTracking */; }; + D2A610AD2BE125F0000AA6AB /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A610AC2BE125F0000AA6AB /* AppConfiguration.swift */; }; + D2A610B02BE12739000AA6AB /* Scenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A610AF2BE12739000AA6AB /* Scenario.swift */; }; + D2A610B22BE14D01000AA6AB /* DefaultScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A610B12BE14D01000AA6AB /* DefaultScenario.swift */; }; + D2C028A02BE14F5700B5D7D3 /* SessionReplayWebViewScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C0289F2BE14F5700B5D7D3 /* SessionReplayWebViewScenario.swift */; }; + D2C028A22BE14FD300B5D7D3 /* SessionReplayWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C028A12BE14FD200B5D7D3 /* SessionReplayWebViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D20B99882BE101690074F98E /* dd-sdk-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "dd-sdk-ios"; path = ..; sourceTree = ""; }; + D2720E2B2BD8FE2F008ADF5D /* E2E.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = E2E.local.xcconfig; sourceTree = ""; }; + D2720E2D2BD8FE2F008ADF5D /* Runner.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Runner.xcconfig; sourceTree = ""; }; + D2720E2E2BD8FE2F008ADF5D /* Synthetics.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Synthetics.xcconfig; sourceTree = ""; }; + D292E3B82BD7BCDF0083453D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D292E3BA2BD7BCDF0083453D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D292E3C82BD7BCE00083453D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D2A610AC2BE125F0000AA6AB /* AppConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; }; + D2A610AF2BE12739000AA6AB /* Scenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scenario.swift; sourceTree = ""; }; + D2A610B12BE14D01000AA6AB /* DefaultScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultScenario.swift; sourceTree = ""; }; + D2C0289F2BE14F5700B5D7D3 /* SessionReplayWebViewScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayWebViewScenario.swift; sourceTree = ""; }; + D2C028A12BE14FD200B5D7D3 /* SessionReplayWebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayWebViewController.swift; sourceTree = ""; }; + D2C028A32BE9282400B5D7D3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D292E3B52BD7BCDF0083453D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A6109D2BE11A74000AA6AB /* DatadogCrashReporting in Frameworks */, + D2A610A52BE11A74000AA6AB /* DatadogSessionReplay in Frameworks */, + D2A610A32BE11A74000AA6AB /* DatadogRUM in Frameworks */, + D2A610A12BE11A74000AA6AB /* DatadogObjc in Frameworks */, + D2A6109F2BE11A74000AA6AB /* DatadogLogs in Frameworks */, + D2A6109B2BE11A74000AA6AB /* DatadogCore in Frameworks */, + D2A610A92BE11A74000AA6AB /* DatadogWebViewTracking in Frameworks */, + D2A610A72BE11A74000AA6AB /* DatadogTrace in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D20B99872BE101690074F98E /* Frameworks */ = { + isa = PBXGroup; + children = ( + D20B99882BE101690074F98E /* dd-sdk-ios */, + ); + name = Frameworks; + sourceTree = ""; + }; + D2720E2F2BD8FE2F008ADF5D /* xcconfigs */ = { + isa = PBXGroup; + children = ( + D2720E2B2BD8FE2F008ADF5D /* E2E.local.xcconfig */, + D2720E2D2BD8FE2F008ADF5D /* Runner.xcconfig */, + D2720E2E2BD8FE2F008ADF5D /* Synthetics.xcconfig */, + ); + path = xcconfigs; + sourceTree = ""; + }; + D292E3782BD7BA830083453D = { + isa = PBXGroup; + children = ( + D2C028A32BE9282400B5D7D3 /* README.md */, + D2720E2F2BD8FE2F008ADF5D /* xcconfigs */, + D292E3B92BD7BCDF0083453D /* Runner */, + D292E3822BD7BA830083453D /* Products */, + D20B99872BE101690074F98E /* Frameworks */, + ); + sourceTree = ""; + }; + D292E3822BD7BA830083453D /* Products */ = { + isa = PBXGroup; + children = ( + D292E3B82BD7BCDF0083453D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + D292E3B92BD7BCDF0083453D /* Runner */ = { + isa = PBXGroup; + children = ( + D292E3BA2BD7BCDF0083453D /* AppDelegate.swift */, + D2A610AC2BE125F0000AA6AB /* AppConfiguration.swift */, + D2A610AE2BE12724000AA6AB /* Scenarios */, + D292E3C82BD7BCE00083453D /* Info.plist */, + ); + path = Runner; + sourceTree = ""; + }; + D2A610AE2BE12724000AA6AB /* Scenarios */ = { + isa = PBXGroup; + children = ( + D2A610AF2BE12739000AA6AB /* Scenario.swift */, + D2A610B12BE14D01000AA6AB /* DefaultScenario.swift */, + D2C0289E2BE14F1B00B5D7D3 /* SessionReplayWebView */, + ); + path = Scenarios; + sourceTree = ""; + }; + D2C0289E2BE14F1B00B5D7D3 /* SessionReplayWebView */ = { + isa = PBXGroup; + children = ( + D2C0289F2BE14F5700B5D7D3 /* SessionReplayWebViewScenario.swift */, + D2C028A12BE14FD200B5D7D3 /* SessionReplayWebViewController.swift */, + ); + path = SessionReplayWebView; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D292E3B72BD7BCDF0083453D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = D292E3CB2BD7BCE00083453D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + D292E3B42BD7BCDF0083453D /* Sources */, + D292E3B52BD7BCDF0083453D /* Frameworks */, + D292E3B62BD7BCDF0083453D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + D2A6109A2BE11A74000AA6AB /* DatadogCore */, + D2A6109C2BE11A74000AA6AB /* DatadogCrashReporting */, + D2A6109E2BE11A74000AA6AB /* DatadogLogs */, + D2A610A02BE11A74000AA6AB /* DatadogObjc */, + D2A610A22BE11A74000AA6AB /* DatadogRUM */, + D2A610A42BE11A74000AA6AB /* DatadogSessionReplay */, + D2A610A62BE11A74000AA6AB /* DatadogTrace */, + D2A610A82BE11A74000AA6AB /* DatadogWebViewTracking */, + ); + productName = Runner; + productReference = D292E3B82BD7BCDF0083453D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D292E3792BD7BA830083453D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + D292E3B72BD7BCDF0083453D = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = D292E37C2BD7BA830083453D /* Build configuration list for PBXProject "E2ETests" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D292E3782BD7BA830083453D; + productRefGroup = D292E3822BD7BA830083453D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D292E3B72BD7BCDF0083453D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D292E3B62BD7BCDF0083453D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D292E3B42BD7BCDF0083453D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D292E3BB2BD7BCDF0083453D /* AppDelegate.swift in Sources */, + D2A610AD2BE125F0000AA6AB /* AppConfiguration.swift in Sources */, + D2C028A02BE14F5700B5D7D3 /* SessionReplayWebViewScenario.swift in Sources */, + D2C028A22BE14FD300B5D7D3 /* SessionReplayWebViewController.swift in Sources */, + D2A610B22BE14D01000AA6AB /* DefaultScenario.swift in Sources */, + D2A610B02BE12739000AA6AB /* Scenario.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D2720E302BD8FE9A008ADF5D /* Synthetics */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Synthetics; + }; + D2720E312BD8FE9A008ADF5D /* Synthetics */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2720E2E2BD8FE2F008ADF5D /* Synthetics.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CURRENT_PROJECT_VERSION = 1; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = JKFCB4CN7C; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog E2E Runner"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.e2e.Runner; + PRODUCT_NAME = "$(TARGET_NAME)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Datadog E2E Runner"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Synthetics; + }; + D292E3A92BD7BA840083453D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D292E3AA2BD7BA840083453D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D292E3C92BD7BCE00083453D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2720E2D2BD8FE2F008ADF5D /* Runner.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JKFCB4CN7C; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog E2E Runner"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.e2e.Runner; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D292E3CA2BD7BCE00083453D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2720E2D2BD8FE2F008ADF5D /* Runner.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JKFCB4CN7C; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog E2E Runner"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.e2e.Runner; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D292E37C2BD7BA830083453D /* Build configuration list for PBXProject "E2ETests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D292E3A92BD7BA840083453D /* Debug */, + D292E3AA2BD7BA840083453D /* Release */, + D2720E302BD8FE9A008ADF5D /* Synthetics */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D292E3CB2BD7BCE00083453D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D292E3C92BD7BCE00083453D /* Debug */, + D292E3CA2BD7BCE00083453D /* Release */, + D2720E312BD8FE9A008ADF5D /* Synthetics */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + D2A6109A2BE11A74000AA6AB /* DatadogCore */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogCore; + }; + D2A6109C2BE11A74000AA6AB /* DatadogCrashReporting */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogCrashReporting; + }; + D2A6109E2BE11A74000AA6AB /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogLogs; + }; + D2A610A02BE11A74000AA6AB /* DatadogObjc */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogObjc; + }; + D2A610A22BE11A74000AA6AB /* DatadogRUM */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogRUM; + }; + D2A610A42BE11A74000AA6AB /* DatadogSessionReplay */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogSessionReplay; + }; + D2A610A62BE11A74000AA6AB /* DatadogTrace */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogTrace; + }; + D2A610A82BE11A74000AA6AB /* DatadogWebViewTracking */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogWebViewTracking; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = D292E3792BD7BA830083453D /* Project object */; +} diff --git a/E2ETests/E2ETests.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/E2ETests/E2ETests.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/E2ETests/E2ETests.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/E2ETests/E2ETests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/E2ETests/E2ETests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/E2ETests/E2ETests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/E2ETests/E2ETests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/E2ETests/E2ETests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..dc55baf0ee --- /dev/null +++ b/E2ETests/E2ETests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/E2ETests/Makefile b/E2ETests/Makefile new file mode 100644 index 0000000000..43af3e81f3 --- /dev/null +++ b/E2ETests/Makefile @@ -0,0 +1,30 @@ +all: dependencies archive upload + +dependencies: + @echo "⚙️ Installing datadog-ci..." + @npm install -g @datadog/datadog-ci + +archive: + xcrun agvtool new-version "$(shell git rev-parse --short HEAD)" + + set -o pipefail && xcodebuild \ + -project E2ETests.xcodeproj \ + -scheme Runner \ + -sdk iphoneos \ + -configuration Synthetics \ + -destination generic/platform=iOS \ + -archivePath .build/Runner.xcarchive \ + archive | xcbeautify + + set -o pipefail && xcodebuild -exportArchive \ + -archivePath .build/Runner.xcarchive \ + -exportOptionsPlist exportOptions.plist \ + -exportPath .build \ + | xcbeautify + +upload: + datadog-ci synthetics upload-application \ + --mobileApp ".build/Runner.ipa" \ + --mobileApplicationId "${S8S_APPLICATION_ID}" \ + --versionName "$(shell agvtool vers -terse)" \ + --latest diff --git a/E2ETests/README.md b/E2ETests/README.md new file mode 100644 index 0000000000..3016f9448f --- /dev/null +++ b/E2ETests/README.md @@ -0,0 +1,88 @@ +# End to End Tests + +[Synthetics for Mobile](https://docs.datadoghq.com/mobile_app_testing/) runs E2E test scenarios. [Monitors](https://docs.datadoghq.com/monitors/) assert the proper propagation of data. + + +## CI + +CI continuously builds, signs, and uploads a runner application to Synthetics which runs predefined tests daily. + +### Build + +Before building the application, the `E2ETests/xcconfigs/E2E.local.xcconfig` configuration file must be present and contain the `Mobile - Integration Org` client token and RUM application ID. These values are sensitive and must be securely stored. + +```ini +CLIENT_TOKEN= +RUM_APPLICATION_ID= +``` + +> [!TIP] +> Files can be base64 encoded and stored in secret variables. To copy the encoded file content, you can do: +> ```bash +> cat E2ETests/xcconfigs/E2E.local.xcconfig | base64 | pbcopy +> ``` + +### Sign + +To sign the runner application, the certificate and provision profile defined in [Synthetics.xcconfig](xcconfigs/Synthetics.xcconfig) and in [exportOptions.plist](exportOptions.plist) needs to be installed on the build machine. These files are sensitive and must be securely stored. Make sure to update both files when updating the certificate and provisioning profile, otherwise signing fails. + +> [!NOTE] +> Certificate & Provisioning Profile are also available through the [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi). But we don't have the tooling in place. + +### Upload + +The application version (build number) is set to the commit SHA of the current job, and the build is uploaded to Synthetics using the [datadog-ci](https://github.com/DataDog/datadog-ci) CLI. This step expects environment variables to authenticate with the `Mobile - Integration Org`: + +```bash +export DATADOG_API_KEY= +export DATADOG_APP_KEY= +export S8S_APPLICATION_ID= +``` + +## Development + +Each scenario is independent and can be considered as an app within the runner. + +### Create a scenario + +A scenario must comply with the [`Scenario`](Runner/Scenarios/Scenario.swift) protocol. Upon start, a scenario initializes the SDK, enables features, and returns a root view-controller. + +Here is a simple example of a scenario using Logs: +```swift +import Foundation +import UIKit + +import DatadogCore +import DatadogLogs + +struct SessionReplayWebViewScenario: Scenario { + + func start(info: TestInfo) -> UIViewController { + + Datadog.initialize( + with: .e2e(info: info), // SDK init with the e2e configuration + trackingConsent: .granted + ) + + Logs.enable() + + return LoggerViewController() + } +} +``` + +The test should then be added to the [`SyntheticScenario`](Runner/Scenarios/Scenario.swift) enumeration so it can be selected, either manually or by setting the `E2E_SCENARIO` environment variable. + + +### Adding the test in synthetics + +**Note:** When creating a test in Synthetics, make sure to always run on the _latest version_. + +You can skip the scenario selection screen by setting the **Process Arguments** of the Synthetic test: +```json +{ + "E2E_SCENARIO": "" +} +``` + +The test's name must match the [`SyntheticScenario`](Runner/Scenarios/Scenario.swift) enum case. \ No newline at end of file diff --git a/E2ETests/Runner/AppConfiguration.swift b/E2ETests/Runner/AppConfiguration.swift new file mode 100644 index 0000000000..add71fbceb --- /dev/null +++ b/E2ETests/Runner/AppConfiguration.swift @@ -0,0 +1,71 @@ +/* + * 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 +import DatadogCore + +/// Test info reads configuration from `Info.plist`. +/// +/// The expected format is as follow: +/// +/// +/// DatadogConfiguration +/// +/// ClientToken +/// $(CLIENT_TOKEN) +/// ApplicationID +/// $(RUM_APPLICATION_ID) +/// Environment +/// $(DD_ENV) +/// Site +/// $(DD_SITE) +/// +/// +struct TestInfo: Decodable { + let clientToken: String + let applicationID: String + let site: DatadogSite + let env: String + + enum CodingKeys: String, CodingKey { + case clientToken = "ClientToken" + case applicationID = "ApplicationID" + case site = "Site" + case env = "Environment" + } +} + +extension TestInfo { + init(bundle: Bundle = .main) throws { + let decoder = AnyDecoder() + let obj = bundle.object(forInfoDictionaryKey: "DatadogConfiguration") + self = try decoder.decode(from: obj) + } +} + +extension TestInfo { + static var empty: Self { + .init( + clientToken: "", + applicationID: "", + site: .us1, + env: "e2e" + ) + } +} + +extension DatadogSite: Decodable {} + +extension Datadog.Configuration { + static func e2e(info: TestInfo) -> Self { + .init( + clientToken: info.clientToken, + env: info.env, + site: info.site + ) + } +} diff --git a/E2ETests/Runner/AppDelegate.swift b/E2ETests/Runner/AppDelegate.swift new file mode 100644 index 0000000000..9b992f4fe0 --- /dev/null +++ b/E2ETests/Runner/AppDelegate.swift @@ -0,0 +1,23 @@ +/* + * 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 UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let info = try! TestInfo() // crash if test info are missing or malformed + + let scenario: Scenario = SyntheticScenario() ?? DefaultScenario() + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = scenario.start(info: info) + window?.makeKeyAndVisible() + return true + } +} diff --git a/E2ETests/Runner/Info.plist b/E2ETests/Runner/Info.plist new file mode 100644 index 0000000000..c5dc3c6338 --- /dev/null +++ b/E2ETests/Runner/Info.plist @@ -0,0 +1,17 @@ + + + + + DatadogConfiguration + + ClientToken + $(CLIENT_TOKEN) + ApplicationID + $(RUM_APPLICATION_ID) + Environment + $(DD_ENV) + Site + $(DD_SITE) + + + diff --git a/E2ETests/Runner/Scenarios/DefaultScenario.swift b/E2ETests/Runner/Scenarios/DefaultScenario.swift new file mode 100644 index 0000000000..d6760fed82 --- /dev/null +++ b/E2ETests/Runner/Scenarios/DefaultScenario.swift @@ -0,0 +1,54 @@ +/* + * 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 UIKit +import SwiftUI + +/// The default scenario will present the list of Synthetic scenarios to run in development mode. +/// To skip this screen, you can set the `E2E_SCENARIO` environment variable with the name +/// the desired scenario. +struct DefaultScenario: Scenario { + func start(info: TestInfo) -> UIViewController { + UIHostingController(rootView: ContentView(info: info)) + } + + struct ContentView: View { + let info: TestInfo + + var body: some View { + NavigationView { + List(SyntheticScenario.allCases, id: \.rawValue) { scenario in + NavigationLink { + ScenarioView(info: info, scenario: scenario) + } label: { + Text(scenario.rawValue) + } + } + .navigationBarTitle("Scenarios") + } + } + } + + struct ScenarioView: UIViewControllerRepresentable { + let info: TestInfo + let scenario: Scenario + + func makeUIViewController(context: Context) -> UIViewController { + scenario.start(info: info) + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } + } +} + +#if DEBUG +struct DefaultScenario_Previews: PreviewProvider { + static var previews: some View { + DefaultScenario.ContentView(info: .empty) + } +} +#endif diff --git a/E2ETests/Runner/Scenarios/Scenario.swift b/E2ETests/Runner/Scenarios/Scenario.swift new file mode 100644 index 0000000000..40e1b2dbfb --- /dev/null +++ b/E2ETests/Runner/Scenarios/Scenario.swift @@ -0,0 +1,68 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import UIKit + +/// A `Scenario` is the entry-point of the E2E runner application. +/// +/// The compliant objects are responsible for initializing the SDK, enabling +/// Features, and create the root view-controller. +protocol Scenario { + /// Starts the scenario. + /// + /// Starting the scenario should intialize the SDK and enable Features based on + /// the provided ``TestInfo`` and scenario's needs. + /// + /// The returned view-controller will be used as the root view controller of the + /// application window. + /// + /// - Parameter info: The test info for configuring the SDK. + /// - Returns: The root view-controller. + func start(info: TestInfo) -> UIViewController +} + +/// A Synthetic scenario can be initialized by defining a Synthetic Test Process Argument +/// named `E2E_SCENARIO`. +/// +/// Note: The raw value of enum case must match the test name defined in Synthetics. +enum SyntheticScenario: String, CaseIterable { + /// The `Session Replay WebView` Synthetics Test. id: `ks5-ba9-ck5` + case sessionReplayWebView + + /// Creates the scenario defined by the`E2E_SCENARIO` environment variable. + /// + /// - Parameter processInfo: The process info holding the environment variables. + init?(processInfo: ProcessInfo = .processInfo) { + guard + processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == nil, // skip SwiftUI preview + let rawValue = processInfo.environment["E2E_SCENARIO"], + let scenario = Self(rawValue: rawValue) + else { + return nil + } + + self = scenario + } + + /// Returns the scenario defined by the environment variable. + var scenario: Scenario { + switch self { + case .sessionReplayWebView: + return SessionReplayWebViewScenario() + } + } +} + +extension SyntheticScenario: Scenario { + /// Starts the underlying scenario. + /// + /// - Parameter info: The test info for configuring the SDK. + /// - Returns: The root view-controller. + func start(info: TestInfo) -> UIViewController { + scenario.start(info: info) + } +} diff --git a/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift b/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift new file mode 100644 index 0000000000..31b7c06b60 --- /dev/null +++ b/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift @@ -0,0 +1,41 @@ +/* + * 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 UIKit +import WebKit +import DatadogWebViewTracking + +class SessionReplayWebViewController: UIViewController, WKUIDelegate { + var webView: WKWebView! + + override func loadView() { + let configuration = WKWebViewConfiguration() + webView = WKWebView(frame: .zero, configuration: configuration) + webView.uiDelegate = self + view = webView + } + + override func viewDidLoad() { + super.viewDidLoad() + WebViewTracking.enable( + webView: webView, + hosts: ["datadoghq.dev"], + sessionReplayConfiguration: WebViewTracking.SessionReplayConfiguration( + privacyLevel: .allow + ) + ) + + let url = URL(string: "https://datadoghq.dev/browser-sdk-test-playground/webview-support")! + let request = URLRequest(url: url) + webView.load(request) + } + + func load(url string: String) { + let url = URL(string: string)! + let request = URLRequest(url: url) + webView.load(request) + } +} diff --git a/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewScenario.swift b/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewScenario.swift new file mode 100644 index 0000000000..4704adbe68 --- /dev/null +++ b/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewScenario.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 Foundation +import UIKit + +import DatadogCore +import DatadogRUM +import DatadogSessionReplay + +struct SessionReplayWebViewScenario: Scenario { + func start(info: TestInfo) -> UIViewController { + Datadog.initialize( + with: .e2e(info: info), + trackingConsent: .granted + ) + + RUM.enable( + with: RUM.Configuration( + applicationID: info.applicationID, + uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(), + uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate() + ) + ) + + SessionReplay.enable( + with: SessionReplay.Configuration( + replaySampleRate: 100, + defaultPrivacyLevel: .allow + ) + ) + + return SessionReplayWebViewController() + } +} diff --git a/E2ETests/exportOptions.plist b/E2ETests/exportOptions.plist new file mode 100644 index 0000000000..9128ee3dcf --- /dev/null +++ b/E2ETests/exportOptions.plist @@ -0,0 +1,19 @@ + + + + + distributionBundleIdentifier + com.datadoghq.e2e.Runner + method + development + provisioningProfiles + + com.datadoghq.e2e.Runner + Datadog E2E Runner + + signingCertificate + Apple Development: Robot Bitrise (9HKDHCMCGH) + teamID + JKFCB4CN7C + + diff --git a/E2ETests/xcconfigs/Runner.xcconfig b/E2ETests/xcconfigs/Runner.xcconfig new file mode 100644 index 0000000000..f5667fcc7a --- /dev/null +++ b/E2ETests/xcconfigs/Runner.xcconfig @@ -0,0 +1,8 @@ +CLIENT_TOKEN = // the Client Token on Mobile Integration Org +RUM_APPLICATION_ID = // the RUM Application ID on Mobile Integration Org + +DD_ENV[config=*] = e2e +DD_ENV[config=Debug] = development +DD_SITE = us1 + +#include? "E2E.local.xcconfig" diff --git a/E2ETests/xcconfigs/Synthetics.xcconfig b/E2ETests/xcconfigs/Synthetics.xcconfig new file mode 100644 index 0000000000..f56ebc4bef --- /dev/null +++ b/E2ETests/xcconfigs/Synthetics.xcconfig @@ -0,0 +1,6 @@ +#include "Runner.xcconfig" + +CODE_SIGN_STYLE = Manual +CODE_SIGN_IDENTITY = Apple Development: Robot Bitrise (9HKDHCMCGH) +DEVELOPMENT_TEAM = JKFCB4CN7C +PROVISIONING_PROFILE_SPECIFIER = Datadog E2E Runner diff --git a/Gemfile.lock b/Gemfile.lock index c19c05ccfe..c7d98e8fa9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,12 +3,13 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.0.8) + activesupport (6.1.7.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.4) + zeitwerk (~> 2.3) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) @@ -65,14 +66,16 @@ GEM i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.3) - minitest (5.20.0) + minitest (5.22.3) molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) public_suffix (4.0.7) - rexml (3.2.5) + rexml (3.2.8) + strscan (>= 3.0.9) ruby-macho (2.5.1) + strscan (3.1.0) typhoeus (1.4.0) ethon (>= 0.9.0) tzinfo (2.0.6) @@ -84,10 +87,12 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) + zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 universal-darwin-22 + universal-darwin-23 x86_64-linux DEPENDENCIES diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj index c98b455696..9d2c26b486 100644 --- a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj +++ b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ diff --git a/Makefile b/Makefile index be0c55047e..4852b8ad31 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,6 @@ all: dependencies templates -# The release version of `dd-sdk-swift-testing` to use for tests instrumentation. -DD_SDK_SWIFT_TESTING_VERSION = 2.3.2 - define DD_SDK_TESTING_XCCONFIG_CI -DD_SDK_TESTING_PATH=$$(DD_SDK_TESTING_OVERRIDE_PATH:default=$$(SRCROOT)/../instrumented-tests/)\n -FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]=$$(inherited) $$(DD_SDK_TESTING_PATH)/DatadogSDKTesting.xcframework/ios-arm64_x86_64-simulator/\n -LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*]=$$(inherited) $$(DD_SDK_TESTING_PATH)/DatadogSDKTesting.xcframework/ios-arm64_x86_64-simulator/\n -FRAMEWORK_SEARCH_PATHS[sdk=appletvsimulator*]=$$(inherited) $$(DD_SDK_TESTING_PATH)/DatadogSDKTesting.xcframework/tvos-arm64_x86_64-simulator/\n -LD_RUNPATH_SEARCH_PATHS[sdk=appletvsimulator*]=$$(inherited) $$(DD_SDK_TESTING_PATH)/DatadogSDKTesting.xcframework/tvos-arm64_x86_64-simulator/\n -OTHER_LDFLAGS[sdk=iphonesimulator*]=$$(inherited) -framework DatadogSDKTesting\n -OTHER_LDFLAGS[sdk=appletvsimulator*]=$$(inherited) -framework DatadogSDKTesting\n DD_TEST_RUNNER=1\n DD_SDK_SWIFT_TESTING_SERVICE=dd-sdk-ios\n DD_SDK_SWIFT_TESTING_APIKEY=${DD_SDK_SWIFT_TESTING_APIKEY}\n @@ -42,7 +32,7 @@ define DD_SDK_BASE_XCCONFIG_CI SWIFT_TREAT_WARNINGS_AS_ERRORS = YES\n \n // If running on CI. This value is injected to some targets through their `Info.plist`:\n -IS_CI = true\n +IS_CI = true\n \n // Use iOS 11 deployment target on CI as long as we use Xcode 14.x for integration\n IPHONEOS_DEPLOYMENT_TARGET=11.0\n @@ -70,15 +60,9 @@ ifeq (${ci}, true) @echo $$DD_SDK_BASE_XCCONFIG_CI >> xcconfigs/Base.local.xcconfig; @echo $$DD_SDK_DATADOG_XCCONFIG_CI > xcconfigs/Datadog.local.xcconfig; ifndef DD_DISABLE_TEST_INSTRUMENTING - @echo $$DD_SDK_TESTING_XCCONFIG_CI > xcconfigs/DatadogSDKTesting.local.xcconfig; - @rm -rf instrumented-tests/DatadogSDKTesting.xcframework - @rm -rf instrumented-tests/DatadogSDKTesting.zip - @rm -rf instrumented-tests/LICENSE - @gh release download ${DD_SDK_SWIFT_TESTING_VERSION} -D instrumented-tests -R https://github.com/DataDog/dd-sdk-swift-testing -p "DatadogSDKTesting.zip" - @unzip -q instrumented-tests/DatadogSDKTesting.zip -d instrumented-tests - @[ -e "instrumented-tests/DatadogSDKTesting.xcframework" ] && echo "DatadogSDKTesting.xcframework - OK" || { echo "DatadogSDKTesting.xcframework - missing"; exit 1; } + @echo $$DD_SDK_TESTING_XCCONFIG_CI > xcconfigs/DatadogSDKTesting.local.xcconfig; endif - + endif # Prepare project on GitLab CI (this will replace `make dependencies` once we're fully on GitLab). @@ -210,3 +194,6 @@ bump: git add . ; \ git commit -m "Bumped version to $$version"; \ echo Bumped version to $$version + +e2e-upload: + ./tools/code-sign.sh -- $(MAKE) -C E2ETests diff --git a/Package.swift b/Package.swift index 87c819d351..17943de813 100644 --- a/Package.swift +++ b/Package.swift @@ -45,7 +45,8 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/microsoft/plcrashreporter.git", from: "1.11.1"), + .package(url: "https://github.com/microsoft/plcrashreporter.git", from: "1.11.2"), + .package(url: "https://github.com/open-telemetry/opentelemetry-swift.git", exact: "1.6.0") ], targets: [ .target( @@ -110,6 +111,7 @@ let package = Package( name: "DatadogTrace", dependencies: [ .target(name: "DatadogInternal"), + .product(name: "OpenTelemetryApi", package: "opentelemetry-swift") ], path: "DatadogTrace/Sources" ), diff --git a/README.md b/README.md index 145e0b182e..0a78f7067e 100644 --- a/README.md +++ b/README.md @@ -22,27 +22,31 @@ ### Log Collection -See the dedicated [Datadog iOS Log Collection](https://docs.datadoghq.com/logs/log_collection/ios) documentation to learn how to send logs from your iOS application to Datadog. +See the dedicated [Datadog iOS Log Collection][1] documentation to learn how to send logs from your iOS application to Datadog. ![Datadog iOS Log Collection](docs/images/logging.png) ### Trace Collection -See [Datadog iOS Trace Collection](https://docs.datadoghq.com/tracing/setup_overview/setup/ios) documentation to try it out. +See [Datadog iOS Trace Collection][2] documentation to try it out. ![Datadog iOS Log Collection](docs/images/tracing.png) ### RUM Events Collection -See [Datadog iOS RUM Collection](https://docs.datadoghq.com/real_user_monitoring/ios) documentation to try it out. +See [Datadog iOS RUM Collection][3] documentation to try it out. ![Datadog iOS RUM Collection](docs/images/rum.png) +#### WebView Tracking + +RUM allows you to monitor web views and eliminate blind spots in your hybrid mobile applications. See [WebView Tracking][5] documentation to try it out. + ## Integrations ### Alamofire -If you use [Alamofire](https://github.com/Alamofire/Alamofire), review the [`Datadog Alamofire Extension` library](DatadogExtensions/Alamofire/) to learn how to automatically instrument requests with the Datadog iOS SDK. +If you use [Alamofire][4], review the [`Datadog Alamofire Extension` library](DatadogExtensions/Alamofire/) to learn how to automatically instrument requests with the Datadog iOS SDK. ## Contributing @@ -51,3 +55,14 @@ Pull requests are welcome. First, open an issue to discuss what you would like t ## License [Apache License, v2.0](LICENSE) + +## Supported Versions + +See the [Supported Versions][6] documentation for more details. + +[1]: https://docs.datadoghq.com/logs/log_collection/ios +[2]: https://docs.datadoghq.com/tracing/setup_overview/setup/ios +[3]: https://docs.datadoghq.com/real_user_monitoring/ios +[4]: https://github.com/Alamofire/Alamofire +[5]: https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/web_view_tracking?tab=ios +[6]: https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/supported_versions/ios/ \ No newline at end of file diff --git a/SUPPORTED-VERSIONS.md b/SUPPORTED-VERSIONS.md deleted file mode 100644 index 6f56125caa..0000000000 --- a/SUPPORTED-VERSIONS.md +++ /dev/null @@ -1,63 +0,0 @@ -## Platforms - -| Platform | Supported | Version | -|------------|:---------:|-------:| -| **iOS** | ✅ | `11+` | -| **tvOS** | ✅ | `11+` | -| **iPadOS** | ✅ | `11+` | -| **macOS (Designed for iPad)** | ✅ | `11+` | -| **macOS (Catalyst)** | ⚠️ | `12+` | -| **macOS** | ⚠️ | `12+` | -| **visionOS** | ⚠️ | `1.0+` | -| **watchOS**| ❌ | `n/a` | -| **Linux** | ❌ | `n/a` | - -## VisionOS - -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 - -SDK is built using the most recent version of Xcode, but we make sure that it's backward compatible with the [lowest supported Xcode version for AppStore submission](https://developer.apple.com/news/?id=jd9wcyov). - -## Dependency Managers - -We currently support integration of the SDK using following dependency managers. -- [Swift Package Manager](https://docs.datadoghq.com/logs/log_collection/ios/?tab=swiftpackagemanagerspm) -- [Cocoapods](https://docs.datadoghq.com/logs/log_collection/ios/?tab=cocoapods) -- [Carthage](https://docs.datadoghq.com/logs/log_collection/ios/?tab=carthage) - -## Languages - -| Language | Version | -|-----------------|:------------:| -| **Swift** | `5.*` | -| **Objective-C** | `2.0` | - -## UI Framework Instrumentation - -| Framework | Automatic | Manual | -|-----------------|:------------:|:------:| -| **UIKit** | ✅ | ✅ | -| **SwiftUI** | ❌ | ✅ | - -## Networking Compatibility -| Framework | Automatic | Manual | -|-----------------|:------------:|:------:| -| **URLSession** | ✅ | ✅ | -|[**Alamofire 5+**](https://github.com/DataDog/dd-sdk-ios/tree/develop/DatadogExtensions/Alamofire) | ❌ | ✅ | -| **SwiftNIO** | ❌ | ❌ | - -*Note: Third party networking libraries can be instrumented by implementing custom `DDURLSessionDelegate`.* - -## Dependencies -The Datadog SDK depends on the following third-party library: -- [PLCrashReporter](https://github.com/microsoft/plcrashreporter) 1.11.1 diff --git a/TestUtilities.podspec b/TestUtilities.podspec index c3ece2d294..59a059083a 100644 --- a/TestUtilities.podspec +++ b/TestUtilities.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = "TestUtilities" - s.version = "2.11.0" + s.version = "2.12.0" s.summary = "Datadog Testing Utilities. This module is for internal testing and should not be published." - + s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" s.license = { :type => "Apache", :file => 'LICENSE' } - s.authors = { + s.authors = { "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", "Maciej Burda" => "maciej.burda@datadoghq.com", "Maxime Epain" => "maxime.epain@datadoghq.com", @@ -25,7 +25,7 @@ Pod::Spec.new do |s| } s.framework = 'XCTest' - + s.source_files = [ "TestUtilities/Helpers/**/*.swift", "TestUtilities/Mocks/**/*.swift", diff --git a/TestUtilities/Helpers/DDAssert.swift b/TestUtilities/Helpers/DDAssert.swift index 3a5c829c6a..bdb60c31f4 100644 --- a/TestUtilities/Helpers/DDAssert.swift +++ b/TestUtilities/Helpers/DDAssert.swift @@ -8,6 +8,7 @@ */ import Foundation +import DatadogInternal import XCTest public enum DDAssertError: Error { @@ -147,6 +148,12 @@ private func _DDAssertJSONEqual(_ expression1: @autoclosure () throws -> T } } +public func DDAssertJSONEqual(_ expression1: @autoclosure () throws -> Any, _ expression2: @autoclosure () throws -> Any, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + _DDEvaluateAssertion(message: message(), file: file, line: line) { + try _DDAssertJSONEqual(AnyCodable(expression1()), AnyCodable(expression2())) + } +} + public func DDAssertJSONEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> U, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where T: Encodable, U: Encodable { _DDEvaluateAssertion(message: message(), file: file, line: line) { try _DDAssertJSONEqual(expression1(), expression2()) diff --git a/TestUtilities/Helpers/ModuleName.swift b/TestUtilities/Helpers/ModuleName.swift new file mode 100644 index 0000000000..c14ae9659e --- /dev/null +++ b/TestUtilities/Helpers/ModuleName.swift @@ -0,0 +1,16 @@ +/* + * 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 + +/// Returns the name of current module. +/// - Returns: The name of caller module. +public func moduleName(file: String = #fileID) -> String { + guard let url = URL(string: file) else { + return "unknown" + } + return url.pathComponents.first ?? "unknown" +} diff --git a/TestUtilities/Mocks/FeatureMessageMocks.swift b/TestUtilities/Mocks/FeatureMessageMocks.swift index 9bb295238a..d5841aa5a2 100644 --- a/TestUtilities/Mocks/FeatureMessageMocks.swift +++ b/TestUtilities/Mocks/FeatureMessageMocks.swift @@ -29,8 +29,13 @@ public extension Array where Element == FeatureMessage { } /// Unpacks the first "telemetry message" in this array. - func firstTelemetry() -> TelemetryMessage? { - return compactMap({ $0.asTelemetry }).first + var firstTelemetry: TelemetryMessage? { + compactMap({ $0.asTelemetry }).first + } + + /// Unpacks the last "telemetry message" in this array. + var lastTelemetry: TelemetryMessage? { + compactMap({ $0.asTelemetry }).last } } diff --git a/TestUtilities/Mocks/TelemetryMocks.swift b/TestUtilities/Mocks/TelemetryMocks.swift index 91c037985d..0801dc201d 100644 --- a/TestUtilities/Mocks/TelemetryMocks.swift +++ b/TestUtilities/Mocks/TelemetryMocks.swift @@ -73,7 +73,7 @@ public class TelemetryMock: Telemetry, CustomStringConvertible { let attributesString = attributes.map({ ", \($0)" }) ?? "" description.append("\n- [debug] \(message)" + attributesString) case .error(_, let message, let kind, let stack): - description.append("\n - [error] \(message), kind: \(kind ?? "nil"), stack: \(stack ?? "nil")") + description.append("\n - [error] \(message), kind: \(kind), stack: \(stack)") case .configuration(let configuration): description.append("\n- [configuration] \(configuration)") case let .metric(name, attributes): @@ -89,10 +89,20 @@ public extension Array where Element == TelemetryMessage { return compactMap({ $0.asMetric }).filter({ $0.name == metricName }).first } - /// Returns attributes of the first ERROR telemetry in this array. + /// Returns attributes of the first debug telemetry in this array. + func firstDebug() -> (id: String, message: String, attributes: [String: Encodable]?)? { + return compactMap { $0.asDebug }.first + } + + /// Returns attributes of the first error telemetry in this array. func firstError() -> (id: String, message: String, kind: String?, stack: String?)? { return compactMap { $0.asError }.first } + + /// Returns the first configuration telemetry in this array. + func firstConfiguration() -> ConfigurationTelemetry? { + return compactMap { $0.asConfiguration }.first + } } public extension TelemetryMessage { diff --git a/bitrise.yml b/bitrise.yml index 718c34f719..e1cb0a9620 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -41,6 +41,7 @@ workflows: after_run: - _make_dependencies - run_conditioned_workflows + - run_e2e_s8s_upload - _deploy_artifacts - _notify_failure_on_slack @@ -535,6 +536,17 @@ workflows: set -e cd tools/distribution && make venv/bin/python3 -m pytest tests + - script: + title: Smoke test dogfooding (with dry-run) + run_if: '{{enveq "DD_RUN_TOOLS_TESTS" "1"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + export DD_DRY_RUN=yes + cd tools/distribution && make + venv/bin/python3 dogfood.py - script: title: Run tests for nightly-unit-tests tool run_if: '{{enveq "DD_RUN_TOOLS_TESTS" "1"}}' @@ -634,3 +646,34 @@ workflows: cd tools/distribution && make venv/bin/python3 release.py "$GIT_TAG" --only-cocoapods + + run_e2e_s8s_upload: + description: |- + Upload E2E application to Synthetics. + steps: + - script: + title: Upload E2E application to Synthetics. + run_if: '{{enveq "BITRISE_GIT_BRANCH" "develop"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + # prepare certificate + export P12_PATH=e2e_cert.p12 + export P12_PASSWORD=$E2E_CERTIFICATE_P12_PASSWORD + echo $E2E_CERTIFICATE_P12_BASE64 | base64 --decode -o $P12_PATH + + # prepare provisioning profile + export PP_PATH=e2e.mobileprovision + echo $E2E_PROVISIONING_PROFILE_BASE64 | base64 --decode -o $PP_PATH + + # prepare xcconfig + echo $E2E_XCCONFIG_BASE64 | base64 --decode -o E2ETests/xcconfigs/E2E.local.xcconfig + + # prepare for synthetics upload + export DATADOG_API_KEY=$E2E_S8S_API_KEY + export DATADOG_APP_KEY=$E2E_S8S_APPLICATION_KEY + export S8S_APPLICATION_ID=$E2E_S8S_APPLICATION_ID + + make e2e-upload diff --git a/dependency-manager-tests/carthage/App/ViewController.swift b/dependency-manager-tests/carthage/App/ViewController.swift index c84b0fe3fd..29cbd2ddf1 100644 --- a/dependency-manager-tests/carthage/App/ViewController.swift +++ b/dependency-manager-tests/carthage/App/ViewController.swift @@ -14,6 +14,7 @@ import DatadogCrashReporting #if os(iOS) import DatadogSessionReplay #endif +import OpenTelemetryApi internal class ViewController: UIViewController { private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional @@ -44,8 +45,24 @@ internal class ViewController: UIViewController { // Trace APIs must be visible: Trace.enable() + // Register tracer provider + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider() + ) + logger.info("It works") - _ = Tracer.shared().startSpan(operationName: "this too") + + let otSpan = Tracer.shared().startSpan(operationName: "OT Span") + otSpan.finish() + + // otel tracer + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + let otelSpan = tracer.spanBuilder(spanName: "OTel span").startSpan() + otelSpan.end() + #if os(iOS) // Session Replay API must be visible: diff --git a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj b/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj index 33cc46ec9a..137e0053b9 100644 --- a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj +++ b/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 3C0F8E222B768A05004948CD /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C0F8E212B768A05004948CD /* OpenTelemetryApi.xcframework */; }; + 3C0F8E232B768A05004948CD /* OpenTelemetryApi.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C0F8E212B768A05004948CD /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C0F8E242B768A17004948CD /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C0F8E212B768A05004948CD /* OpenTelemetryApi.xcframework */; }; + 3C0F8E252B768A17004948CD /* OpenTelemetryApi.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C0F8E212B768A05004948CD /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3CB135E729F6B90F0000234F /* DatadogWebViewTracking.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CB135E429F6B8F90000234F /* DatadogWebViewTracking.xcframework */; }; 3CB135E829F6B90F0000234F /* DatadogWebViewTracking.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CB135E429F6B8F90000234F /* DatadogWebViewTracking.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 617803D22A6FF2EA005FE258 /* DatadogSessionReplay.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 617803D12A6FF2EA005FE258 /* DatadogSessionReplay.xcframework */; }; @@ -103,6 +107,7 @@ 9E9D5E8D25F90FC6002F12A0 /* DatadogCore.xcframework in Embed Frameworks */, D20D6FEB29F6C2F200D2886E /* DatadogRUM.xcframework in Embed Frameworks */, D2675BF42A019CF500190669 /* DatadogCrashReporting.xcframework in Embed Frameworks */, + 3C0F8E232B768A05004948CD /* OpenTelemetryApi.xcframework in Embed Frameworks */, 9E9D5E8B25F90FC6002F12A0 /* DatadogObjc.xcframework in Embed Frameworks */, D26F741929ACC61E00D25622 /* DatadogLogs.xcframework in Embed Frameworks */, ); @@ -116,6 +121,7 @@ dstSubfolderSpec = 10; files = ( D290BA2627CD09740019936D /* CrashReporter.xcframework in Embed Frameworks */, + 3C0F8E252B768A17004948CD /* OpenTelemetryApi.xcframework in Embed Frameworks */, D20D6FE729F6C2EB00D2886E /* DatadogRUM.xcframework in Embed Frameworks */, D20D6FE929F6C2ED00D2886E /* DatadogTrace.xcframework in Embed Frameworks */, D2675BFA2A019D0100190669 /* DatadogInternal.xcframework in Embed Frameworks */, @@ -130,6 +136,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3C0F8E212B768A05004948CD /* OpenTelemetryApi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = OpenTelemetryApi.xcframework; path = Carthage/Build/OpenTelemetryApi.xcframework; sourceTree = ""; }; 3CB135E429F6B8F90000234F /* DatadogWebViewTracking.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DatadogWebViewTracking.xcframework; path = Carthage/Build/DatadogWebViewTracking.xcframework; sourceTree = ""; }; 615519322461CDB4002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 615519332461CDB4002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; @@ -172,6 +179,7 @@ D20D6FEA29F6C2F200D2886E /* DatadogRUM.xcframework in Frameworks */, 3CB135E729F6B90F0000234F /* DatadogWebViewTracking.xcframework in Frameworks */, 9E9D5E8C25F90FC6002F12A0 /* DatadogCore.xcframework in Frameworks */, + 3C0F8E222B768A05004948CD /* OpenTelemetryApi.xcframework in Frameworks */, D2675BF32A019CF500190669 /* DatadogCrashReporting.xcframework in Frameworks */, 9E9D5E8A25F90FC6002F12A0 /* DatadogObjc.xcframework in Frameworks */, ); @@ -196,6 +204,7 @@ buildActionMask = 2147483647; files = ( D2B946AE29ACF6C20080CB40 /* DatadogLogs.xcframework in Frameworks */, + 3C0F8E242B768A17004948CD /* OpenTelemetryApi.xcframework in Frameworks */, D20D6FE829F6C2ED00D2886E /* DatadogTrace.xcframework in Frameworks */, D2675BF92A019D0100190669 /* DatadogInternal.xcframework in Frameworks */, D290BA1F27CD09740019936D /* CrashReporter.xcframework in Frameworks */, @@ -291,6 +300,7 @@ 61C364492437547A00C4D4E6 /* Frameworks */ = { isa = PBXGroup; children = ( + 3C0F8E212B768A05004948CD /* OpenTelemetryApi.xcframework */, 617803D12A6FF2EA005FE258 /* DatadogSessionReplay.xcframework */, D20D6FE329F6C2D600D2886E /* DatadogRUM.xcframework */, D2966C2329CA1C5300FC6B3C /* DatadogTrace.xcframework */, diff --git a/dependency-manager-tests/carthage/Makefile b/dependency-manager-tests/carthage/Makefile index 9597ebd487..e1c5891d05 100644 --- a/dependency-manager-tests/carthage/Makefile +++ b/dependency-manager-tests/carthage/Makefile @@ -30,4 +30,5 @@ test: @[ -e "Carthage/Build/DatadogCrashReporting.xcframework" ] && echo "DatadogCrashReporting.xcframework - OK" || { echo "DatadogCrashReporting.xcframework - missing"; false; } @[ -e "Carthage/Build/CrashReporter.xcframework" ] && echo "CrashReporter.xcframework - OK" || { echo "CrashReporter.xcframework - missing"; false; } @[ -e "Carthage/Build/DatadogWebViewTracking.xcframework" ] && echo "DatadogWebViewTracking.xcframework - OK" || { echo "DatadogWebViewTracking.xcframework - missing"; false; } + @[ -e "Carthage/Build/OpenTelemetryApi.xcframework" ] && echo "OpenTelemetryApi.xcframework - OK" || { echo "OpenTelemetryApi.xcframework - missing"; false; } @echo "🧪 SUCCEEDED" diff --git a/dependency-manager-tests/cocoapods/App/ViewController.swift b/dependency-manager-tests/cocoapods/App/ViewController.swift index e14ceb32ca..80617a6935 100644 --- a/dependency-manager-tests/cocoapods/App/ViewController.swift +++ b/dependency-manager-tests/cocoapods/App/ViewController.swift @@ -14,6 +14,7 @@ import DatadogCrashReporting import DatadogSessionReplay // it should compile for iOS and tvOS, but APIs are only available on iOS import DatadogObjc import Alamofire +import OpenTelemetryApi internal class ViewController: UIViewController { private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional @@ -44,9 +45,24 @@ internal class ViewController: UIViewController { // Trace APIs must be visible: Trace.enable() + // Register tracer provider + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider() + ) + logger.info("It works") - _ = Tracer.shared().startSpan(operationName: "this too") + let otSpan = Tracer.shared().startSpan(operationName: "OT Span") + otSpan.finish() + + // otel tracer + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + let otelSpan = tracer.spanBuilder(spanName: "OTel span").startSpan() + otelSpan.end() + #if os(iOS) SessionReplay.enable(with: .init(replaySampleRate: 0)) #endif diff --git a/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj b/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj index 7d2745b114..f7443e781d 100644 --- a/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj +++ b/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj @@ -390,7 +390,8 @@ 61373B2526E0E78300E0F46E /* Sources */, 61373B2626E0E78300E0F46E /* Frameworks */, 61373B2726E0E78300E0F46E /* Resources */, - CC86EE069424642723D0C2E2 /* [CP] Copy Pods Resources */, + 9AB54E91F6CF9F39B153DDCF /* [CP] Copy Pods Resources */, + 8A0FC3D9BCDA15BFE1388572 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -447,7 +448,7 @@ 61B8C30026E0E278006EDF53 /* Frameworks */, 61B8C30126E0E278006EDF53 /* Resources */, 31384BB2056A865F3ED23F38 /* [CP] Embed Pods Frameworks */, - A242A29C234C1DB8379AA6B1 /* [CP] Copy Pods Resources */, + CE173FAE86F3C55AD4217B8F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -595,7 +596,8 @@ D245427827C8E93D0039E0A6 /* Sources */, D245427C27C8E93D0039E0A6 /* Frameworks */, D245427E27C8E93D0039E0A6 /* Resources */, - 5B109A73B29580A024EBCD59 /* [CP] Copy Pods Resources */, + 902D6AE45E60E1B9A2AD5BD5 /* [CP] Copy Pods Resources */, + CBDFC631D4E69499312261B3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -828,21 +830,21 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 5B109A73B29580A024EBCD59 /* [CP] Copy Pods Resources */ = { + 8A0FC3D9BCDA15BFE1388572 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-resources-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Common-App Static iOS/Pods-Common-App Static iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Common-App Static iOS/Pods-Common-App Static iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Static iOS/Pods-Common-App Static iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 8D0504D0CD7CAFBBB5B63F13 /* [CP] Check Pods Manifest.lock */ = { @@ -867,24 +869,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A242A29C234C1DB8379AA6B1 /* [CP] Copy Pods Resources */ = { + 902D6AE45E60E1B9A2AD5BD5 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Common-App Dynamic iOS/Pods-Common-App Dynamic iOS-resources-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Common-App Dynamic iOS/Pods-Common-App Dynamic iOS-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Dynamic iOS/Pods-Common-App Dynamic iOS-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-resources.sh\"\n"; showEnvVarsInLog = 0; }; - CC86EE069424642723D0C2E2 /* [CP] Copy Pods Resources */ = { + 9AB54E91F6CF9F39B153DDCF /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -901,6 +903,40 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Static iOS/Pods-Common-App Static iOS-resources.sh\"\n"; showEnvVarsInLog = 0; }; + CBDFC631D4E69499312261B3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Static tvOS/Pods-Common-App Static tvOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + CE173FAE86F3C55AD4217B8F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Common-App Dynamic iOS/Pods-Common-App Dynamic iOS-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Common-App Dynamic iOS/Pods-Common-App Dynamic iOS-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-App Dynamic iOS/Pods-Common-App Dynamic iOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; D235937827C8EB0500BF32D7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/dependency-manager-tests/cocoapods/Podfile.src b/dependency-manager-tests/cocoapods/Podfile.src index da859a6469..fc2dbb0d17 100644 --- a/dependency-manager-tests/cocoapods/Podfile.src +++ b/dependency-manager-tests/cocoapods/Podfile.src @@ -1,4 +1,5 @@ abstract_target 'Common' do + pod 'OpenTelemetrySwiftApi', '1.6.0' pod 'DatadogInternal', :git => 'GIT_REMOTE', :GIT_REFERENCE pod 'DatadogCore', :git => 'GIT_REMOTE', :GIT_REFERENCE pod 'DatadogLogs', :git => 'GIT_REMOTE', :GIT_REFERENCE diff --git a/dependency-manager-tests/spm/App/ViewController.swift b/dependency-manager-tests/spm/App/ViewController.swift index 5e1a17c4d5..eafeaf9d5e 100644 --- a/dependency-manager-tests/spm/App/ViewController.swift +++ b/dependency-manager-tests/spm/App/ViewController.swift @@ -8,6 +8,8 @@ import UIKit import DatadogRUM import DatadogObjc import DatadogSessionReplay // it should compile for iOS and tvOS, but APIs are only available on iOS +import DatadogTrace +import OpenTelemetryApi internal class ViewController: UIViewController { override func viewDidLoad() { @@ -17,7 +19,24 @@ internal class ViewController: UIViewController { // RUM APIs must be visible: RUM.enable(with: .init(applicationID: "app-id")) RUMMonitor.shared().startView(viewController: self) + + // Trace APIs must be visible: + Trace.enable() + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider() + ) + + let otSpan = Tracer.shared().startSpan(operationName: "OT Span") + otSpan.finish() + // otel tracer + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + let otelSpan = tracer.spanBuilder(spanName: "OTel span").startSpan() + otelSpan.end() + #if os(iOS) // Session Replay API must be visible: SessionReplay.enable(with: .init(replaySampleRate: 0)) diff --git a/dependency-manager-tests/xcframeworks/App/ViewController.swift b/dependency-manager-tests/xcframeworks/App/ViewController.swift index 29a41b0a9d..b5501cc721 100644 --- a/dependency-manager-tests/xcframeworks/App/ViewController.swift +++ b/dependency-manager-tests/xcframeworks/App/ViewController.swift @@ -14,6 +14,7 @@ import DatadogCrashReporting #if os(iOS) import DatadogSessionReplay #endif +import OpenTelemetryApi internal class ViewController: UIViewController { private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional @@ -44,8 +45,23 @@ internal class ViewController: UIViewController { // Trace APIs must be visible: Trace.enable() + // Register tracer provider + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider() + ) + logger.info("It works") - _ = Tracer.shared().startSpan(operationName: "this too") + + let otSpan = Tracer.shared().startSpan(operationName: "OT Span") + otSpan.finish() + + // otel tracer + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + let otelSpan = tracer.spanBuilder(spanName: "OTel Span").startSpan() + otelSpan.end() #if os(iOS) SessionReplay.enable(with: .init(replaySampleRate: 0)) diff --git a/dependency-manager-tests/xcframeworks/Makefile b/dependency-manager-tests/xcframeworks/Makefile index 2f129db523..b195057a5e 100644 --- a/dependency-manager-tests/xcframeworks/Makefile +++ b/dependency-manager-tests/xcframeworks/Makefile @@ -26,4 +26,5 @@ test: @[ -e "dd-sdk-ios/build/xcframeworks/DatadogObjc.xcframework" ] && echo "DatadogObjc.xcframework - OK" || { echo "DatadogObjc.xcframework - missing"; false; } @[ -e "dd-sdk-ios/build/xcframeworks/DatadogCrashReporting.xcframework" ] && echo "DatadogCrashReporting.xcframework - OK" || { echo "DatadogCrashReporting.xcframework - missing"; false; } @[ -e "dd-sdk-ios/build/xcframeworks/CrashReporter.xcframework" ] && echo "CrashReporter.xcframework - OK" || { echo "CrashReporter.xcframework - missing"; false; } + @[ -e "dd-sdk-ios/build/xcframeworks/OpenTelemetryApi.xcframework" ] && echo "OpenTelemetryApi.xcframework - OK" || { echo "OpenTelemetryApi.xcframework - missing"; false; } @echo "🧪 SUCCEEDED" diff --git a/dependency-manager-tests/xcframeworks/XCProject.xcodeproj/project.pbxproj b/dependency-manager-tests/xcframeworks/XCProject.xcodeproj/project.pbxproj index 51fc2c3c9d..e5fcf1f276 100644 --- a/dependency-manager-tests/xcframeworks/XCProject.xcodeproj/project.pbxproj +++ b/dependency-manager-tests/xcframeworks/XCProject.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 3C14BEF42B76815800D8F265 /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C14BEF32B76815800D8F265 /* OpenTelemetryApi.xcframework */; }; + 3C14BEF52B76815800D8F265 /* OpenTelemetryApi.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C14BEF32B76815800D8F265 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3CAD15512B7BCAE4006480B5 /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C14BEF32B76815800D8F265 /* OpenTelemetryApi.xcframework */; }; + 3CAD15522B7BCAE4006480B5 /* OpenTelemetryApi.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C14BEF32B76815800D8F265 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 617803D52A701052005FE258 /* DatadogSessionReplay.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 617803D42A701051005FE258 /* DatadogSessionReplay.xcframework */; }; 617803D62A701052005FE258 /* DatadogSessionReplay.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 617803D42A701051005FE258 /* DatadogSessionReplay.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 61C36419243752A500C4D4E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C36418243752A500C4D4E6 /* AppDelegate.swift */; }; @@ -103,6 +107,7 @@ D2F09A2629F6C65B0036B910 /* DatadogRUM.xcframework in Embed Frameworks */, D2F9244B29A4B9A4006733B2 /* CrashReporter.xcframework in Embed Frameworks */, D2675BFE2A019F7500190669 /* DatadogWebViewTracking.xcframework in Embed Frameworks */, + 3C14BEF52B76815800D8F265 /* OpenTelemetryApi.xcframework in Embed Frameworks */, D2675C002A01A03300190669 /* DatadogCrashReporting.xcframework in Embed Frameworks */, D2EBEDAF29B7867700B15732 /* DatadogInternal.xcframework in Embed Frameworks */, ); @@ -116,6 +121,7 @@ dstSubfolderSpec = 10; files = ( D2F9245A29A4B9D8006733B2 /* DatadogObjc.xcframework in Embed Frameworks */, + 3CAD15522B7BCAE4006480B5 /* OpenTelemetryApi.xcframework in Embed Frameworks */, D2EBEDAC29B7863B00B15732 /* DatadogLogs.xcframework in Embed Frameworks */, D2F9245629A4B9D8006733B2 /* DatadogCore.xcframework in Embed Frameworks */, D2966C2C29CA1E1C00FC6B3C /* DatadogTrace.xcframework in Embed Frameworks */, @@ -130,6 +136,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3C14BEF32B76815800D8F265 /* OpenTelemetryApi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = OpenTelemetryApi.xcframework; path = "dd-sdk-ios/build/xcframeworks/OpenTelemetryApi.xcframework"; sourceTree = ""; }; 3C4C2BB629F6B9C100152C4B /* DatadogWebViewTracking.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DatadogWebViewTracking.xcframework; path = "dd-sdk-ios/build/xcframeworks/DatadogWebViewTracking.xcframework"; sourceTree = ""; }; 615519322461CDB4002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 615519332461CDB4002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; @@ -172,6 +179,7 @@ D2F09A2529F6C65B0036B910 /* DatadogRUM.xcframework in Frameworks */, D2F9244A29A4B9A4006733B2 /* CrashReporter.xcframework in Frameworks */, D2675BFD2A019F7500190669 /* DatadogWebViewTracking.xcframework in Frameworks */, + 3C14BEF42B76815800D8F265 /* OpenTelemetryApi.xcframework in Frameworks */, D2675BFF2A01A03300190669 /* DatadogCrashReporting.xcframework in Frameworks */, D2EBEDAE29B7867700B15732 /* DatadogInternal.xcframework in Frameworks */, ); @@ -196,6 +204,7 @@ buildActionMask = 2147483647; files = ( D2F9245929A4B9D8006733B2 /* DatadogObjc.xcframework in Frameworks */, + 3CAD15512B7BCAE4006480B5 /* OpenTelemetryApi.xcframework in Frameworks */, D2EBEDAB29B7863B00B15732 /* DatadogLogs.xcframework in Frameworks */, D2F9245529A4B9D8006733B2 /* DatadogCore.xcframework in Frameworks */, D2966C2B29CA1E1C00FC6B3C /* DatadogTrace.xcframework in Frameworks */, @@ -291,6 +300,7 @@ 61C364492437547A00C4D4E6 /* Frameworks */ = { isa = PBXGroup; children = ( + 3C14BEF32B76815800D8F265 /* OpenTelemetryApi.xcframework */, 617803D42A701051005FE258 /* DatadogSessionReplay.xcframework */, D2F09A2429F6C65A0036B910 /* DatadogRUM.xcframework */, D2966C2829CA1E1600FC6B3C /* DatadogTrace.xcframework */, diff --git a/docs/rum_collection/web_view_tracking.md b/docs/rum_collection/web_view_tracking.md deleted file mode 100644 index bc40260e18..0000000000 --- a/docs/rum_collection/web_view_tracking.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -beta: true -dependencies: -- https://github.com/DataDog/dd-sdk-ios/blob/master/docs/web_view_tracking.md -title: iOS Web View Tracking -kind: documentation -description: Monitor web views in your hybrid iOS applications. -further_reading: -- link: "/real_user_monitoring/ios/" - tag: "Documentation" - text: "iOS Monitoring" -- link: "/real_user_monitoring/browser/" - tag: "Documentation" - text: "Browser Monitoring" ---- - -## Overview - -Real User Monitoring allows you to monitor web views and eliminate blind spots in your hybrid iOS and tvOS applications. - -You can perform the following: - -- Track user journeys across web and native components in mobile applications -- Scope the root cause of latency to web pages or native components in mobile applications -- Support users that have difficulty loading web pages on mobile devices - -## Setup - -### Prerequisites - -Set up the web page you want rendered on your mobile iOS and tvOS application with the RUM Browser SDK first. For more information, see [RUM Browser Monitoring][1]. - -### Instrument your web views - -The RUM iOS SDK provides APIs for you to control web view tracking. To add Web View Tracking, declare the following as an extension of `WKUserContentController`. - -`trackDatadogEvents(in hosts: Set)` -: Enables RUM event tracking in a web view for certain `hosts`. - -`stopTrackingDatadogEvents()` -: Disables RUM event tracking in a web view. When a web view is about to be de-allocated or you are done with the web view, call this API. - -For example: - -``` -import WebKit -import DatadogCore - -webView.configuration.userContentController.trackDatadogEvents(in: ["example.com"]) -``` - -## Access your web views - -Your web views appear as events and views in the [RUM Explorer][4] with associated `service` and `source` attributes. The `service` attribute indicates the web component the web view is generated from, and the `source` attribute denotes the mobile application's platform, such as iOS. - -Filter on your iOS and tvOS applications, and click a session. A side panel with a list of events in the session appears. - -{{< img src="real_user_monitoring/ios/ios-webview-tracking.png" alt="Webview events captured in a session in the RUM Explorer" style="width:100%;">}} - -Click **Open View waterfall** to navigate from the session to a resource waterfall visualization in the view's **Performance** tab. - -## Further Reading - -{{< partial name="whats-next/whats-next.html" >}} - -[1]: https://docs.datadoghq.com/real_user_monitoring/browser/#npm -[2]: https://github.com/DataDog/dd-sdk-ios/releases/tag/1.10.0-beta1 -[3]: https://docs.datadoghq.com/real_user_monitoring/ios/ -[4]: https://app.datadoghq.com/rum/explorer diff --git a/tools/code-sign.sh b/tools/code-sign.sh new file mode 100755 index 0000000000..4b7b2aa8b4 --- /dev/null +++ b/tools/code-sign.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +function usage() { + cat << EOF +OVERVIEW: Install Apple certificate and provisioning profile to build and sign. + +EXAMPLE: $(basename "${BASH_SOURCE[0]}") -- make export + +USAGE: $(basename "${BASH_SOURCE[0]}") [--p12 ] [--p12-password ] [--provisioning-profile] -- + +OPTIONS: + +-h, --help Print this help and exit. +--p12 Path to Apple signing 'p12' certificate. env P12_PATH +--p12-password The password for yotheur Apple signing certificate. env P12_PASSWORD +--provisioning-profile Path to Apple provisioning profile. env PP_PATH + +EOF + exit +} + +# read cmd arguments +while :; do + case $1 in + --p12) P12_PATH=$2 + shift + ;; + --p12-password) P12_PASSWORD=$2 + shift + ;; + --provisioning-profile) PP_PATH=$2 + shift + ;; + -h|--help) usage + shift + ;; + --) shift + CMD=$@ + break + ;; + *) break + esac + shift +done + +if [ -z "$P12_PATH" ] || [ -z "$P12_PASSWORD" ] || [ -z "$PP_PATH" ] || [ -z "$CMD" ]; then usage; fi + +# Ensure we do not leak any secrets +set +x -e + +KEYCHAIN=datadog.keychain +KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" +PROFILE=datadog.mobileprovision + +cleanup() { + rm -f ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE + security delete-keychain $KEYCHAIN +} + +# clean up keychain and provisioning profile on exit +trap cleanup EXIT + +# create temporary keychain +security delete-keychain $KEYCHAIN || : +security create-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN +security set-keychain-settings -lut 21600 $KEYCHAIN +security unlock-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN + +# import certificate to keychain +security import $P12_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN +security list-keychain -d user -s $KEYCHAIN "login.keychain" "System.keychain" +security set-key-partition-list -S apple-tool:,apple: -s -k $KEYCHAIN_PASSWORD $KEYCHAIN >/dev/null 2>&1 + +# apply provisioning profile +mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles +cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE + +# run command with certificate and provisioning profile available +exec $CMD diff --git a/tools/distribution/build-xcframework.sh b/tools/distribution/build-xcframework.sh index 04a58aa4d4..1a588c6eb4 100755 --- a/tools/distribution/build-xcframework.sh +++ b/tools/distribution/build-xcframework.sh @@ -90,6 +90,7 @@ rm -rf $OUTPUT carthage bootstrap --platform $PLATFORM --use-xcframeworks mkdir -p "$XCFRAMEWORK_OUTPUT" cp -r "Carthage/Build/CrashReporter.xcframework" "$XCFRAMEWORK_OUTPUT" +cp -r "Carthage/Build/OpenTelemetryApi.xcframework" "$XCFRAMEWORK_OUTPUT" bundle DatadogInternal bundle DatadogCore diff --git a/tools/distribution/dogfood.py b/tools/distribution/dogfood.py index 0b5bca1365..1a9eb9dcf1 100755 --- a/tools/distribution/dogfood.py +++ b/tools/distribution/dogfood.py @@ -29,9 +29,9 @@ def dogfood(dry_run: bool, repository_url: str, repository_name: str, repository dd_sdk_ios_package = PackageResolvedFile(path=f'{dd_sdk_package_path}/Package.resolved') dd_sdk_ios_package.print() - if dd_sdk_ios_package.version > 2: + if dd_sdk_ios_package.version > 3: raise Exception( - f'`dogfood.py` expects the `package.resolved` in `dd-sdk-ios` to use version <= 2 ' + + f'`dogfood.py` expects the `package.resolved` in `dd-sdk-ios` to use version <= 3 ' + f'but version {dd_sdk_ios_package.version} was detected. Update `dogfood.py` to use this version.' ) @@ -73,7 +73,7 @@ def dogfood(dry_run: bool, repository_url: str, repository_name: str, repository else: package.add_dependency( package_id=dependency_id, - repository_url=dependency['repositoryURL'], + repository_url=dependency['location'], branch=dependency['state'].get('branch'), revision=dependency['state']['revision'], version=dependency['state'].get('version'), @@ -111,6 +111,8 @@ def dogfood(dry_run: bool, repository_url: str, repository_name: str, repository try: dry_run = os.environ.get('DD_DRY_RUN') == 'yes' + if dry_run: + print(f'ℹ️ Running in dry-run mode') skip_datadog_ios = os.environ.get('DD_SKIP_DATADOG_IOS') == 'yes' skip_shopist_ios = os.environ.get('DD_SKIP_SHOPIST_IOS') == 'yes' diff --git a/tools/distribution/src/dogfood/package_resolved.py b/tools/distribution/src/dogfood/package_resolved.py index a47dc2f8d6..225e452ce9 100644 --- a/tools/distribution/src/dogfood/package_resolved.py +++ b/tools/distribution/src/dogfood/package_resolved.py @@ -88,10 +88,12 @@ def __init__(self, path: str): self.wrapped = PackageResolvedContentV1(self.path, self.packages) elif self.version == 2: self.wrapped = PackageResolvedContentV2(self.path, self.packages) + elif self.version == 3: + self.wrapped = PackageResolvedContentV3(self.path, self.packages) else: raise Exception( f'{path} uses version {self.version} but `PackageResolvedFile` only supports ' + - f'versions `1` and `2`. Update `PackageResolvedFile` to support new version.' + f'versions `1`, `2` and `3`. Update `PackageResolvedFile` to support new version.' ) def save(self): @@ -132,6 +134,15 @@ def read_dependency_ids(self) -> [PackageID]: def read_dependency(self, package_id: PackageID) -> dict: return self.wrapped.read_dependency(package_id) + def origin_hash(self) -> str: + if self.version == 3: + return self.wrapped.origin_hash() + else: + raise Exception( + f'{self.path} does not contain `originHash` property. ' + + f'Check if the `package.resolved` file is in version 3.' + ) + class PackageResolvedContentV1(PackageResolvedContent): """ @@ -346,3 +357,32 @@ def __get_package(self, package_id: PackageID): package_pin_index = package_pins[0] return self.packages['pins'][package_pin_index] + + +class PackageResolvedContentV3(PackageResolvedContentV2): + """ + Example of `package.resolved` in version `3` looks this:: + + { + "originHash" : "b47de6af98c4a9811a8d2af11d70b960dfc66b7c8e4944b35bb74c8f8bb9c487", + "pins" : [ + { + "identity" : "dd-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/dd-sdk-ios", + "state" : { + "branch" : "dogfooding", + "revision" : "6f662103771eb4523164e64f7f936bf9276f6bd0" + } + }, + ... + ] + "version" : 3 + } + + In v3 new property origin hash has been added which SHA256 hash of the repository content. + Check https://github.com/apple/swift-package-manager/blob/e5123e483c18bff7afdc3b2029f9e1924779bbc8/Sources/Workspace/Workspace%2BDependencies.swift#L280 + """ + + def origin_hash(self): + return self.packages['originHash'] diff --git a/tools/distribution/tests/dogfood/test_package_resolved.py b/tools/distribution/tests/dogfood/test_package_resolved.py index c5641dfece..a6874a9d11 100644 --- a/tools/distribution/tests/dogfood/test_package_resolved.py +++ b/tools/distribution/tests/dogfood/test_package_resolved.py @@ -65,6 +65,33 @@ class PackageResolvedFileTestCase(unittest.TestCase): } ''' + v3_file_content = b''' + { + "originHash" : "ea83017c944c7850b8f60207e6143eb17cb6b5e6b734b3fa08787a5d920dba7b", + "pins" : [ + { + "identity" : "a", + "kind" : "remoteSourceControl", + "location" : "https://github.com/A-org/a", + "state" : { + "branch" : "a-branch", + "revision" : "a-revision" + } + }, + { + "identity" : "b", + "kind" : "remoteSourceControl", + "location" : "https://github.com/B-org/b.git", + "state" : { + "revision" : "b-revision", + "version" : "1.0.0" + } + } + ], + "version" : 3 + } + ''' + def test_it_reads_version1_files(self): with NamedTemporaryFile() as file: file.write(self.v1_file_content) @@ -264,3 +291,39 @@ def test_v2_package_id_from_repository_url(self): self.assertEqual('abc', v2_package_id_from_repository_url(repository_url='https://github.com/A-org/abc')) self.assertEqual('abc', v2_package_id_from_repository_url(repository_url='git@github.com:DataDog/abc.git')) self.assertEqual('abc', v2_package_id_from_repository_url(repository_url='git@github.com:DataDog/abc')) + + def test_it_reads_version3_files(self): + with NamedTemporaryFile() as file: + file.write(self.v3_file_content) + file.seek(0) + + package_resolved = PackageResolvedFile(path=file.name) + self.assertTrue(package_resolved.has_dependency(package_id=PackageID(v1=None, v2='a'))) + self.assertTrue(package_resolved.has_dependency(package_id=PackageID(v1=None, v2='b'))) + self.assertFalse(package_resolved.has_dependency(package_id=PackageID(v1=None, v2='c'))) + self.assertListEqual( + [PackageID(v1=None, v2='a'), PackageID(v1=None, v2='b')], + package_resolved.read_dependency_ids() + ) + self.assertDictEqual( + { + 'identity': 'a', + 'kind': 'remoteSourceControl', + 'location': 'https://github.com/A-org/a', + 'state': {'branch': 'a-branch', 'revision': 'a-revision'} + }, + package_resolved.read_dependency(package_id=PackageID(v1=None, v2='a')) + ) + self.assertDictEqual( + { + 'identity': 'b', + 'kind': 'remoteSourceControl', + 'location': 'https://github.com/B-org/b.git', + 'state': {'revision': 'b-revision', 'version': '1.0.0'} + }, + package_resolved.read_dependency(PackageID(v1=None, v2='b')) + ) + self.assertEqual( + "ea83017c944c7850b8f60207e6143eb17cb6b5e6b734b3fa08787a5d920dba7b", + package_resolved.origin_hash() + ) \ No newline at end of file diff --git a/tools/lint/sources.swiftlint.yml b/tools/lint/sources.swiftlint.yml index 230c323139..9ff9dab540 100644 --- a/tools/lint/sources.swiftlint.yml +++ b/tools/lint/sources.swiftlint.yml @@ -89,6 +89,15 @@ custom_rules: - doccomment message: "`UIApplication.shared` is unavailable in some environments. Check `UIApplication.managedShared`." severity: error + unsafe_all_http_header_fields: # prevents from using `URLRequest.allHTTPHeaderFields` API + included: Sources + name: "Unsafe API: `URLRequest.allHTTPHeaderFields`" + regex: '\.allHTTPHeaderFields' + excluded_match_kinds: + - comment + - doccomment + message: "`URLRequest.allHTTPHeaderFields` is considered unsafe. Prefer using `URLRequest.value(forHTTPHeaderField:)`. See full context in https://github.com/DataDog/dd-sdk-ios/pull/1843" + severity: error required_reason_api_name: # prevents from declaring symbols that may conflict with Apple's Required Reason APIs included: Sources name: "Required Reason API Conflict" diff --git a/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift b/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift index f551140b58..aa22d6ad24 100644 --- a/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift +++ b/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift @@ -26,6 +26,8 @@ public class RUMCodeDecorator: SwiftCodeDecorator { "RUMOperatingSystem", "RUMActionID", "RUMSessionPrecondition", + "RUMTelemetryDevice", + "RUMTelemetryOperatingSystem", ] ) } @@ -104,11 +106,25 @@ public class RUMCodeDecorator: SwiftCodeDecorator { } if fixedName == "Device" { - fixedName = "RUMDevice" + if context.parent?.typeName == "telemetry" { + // The `telemetry.device` added in https://github.com/DataDog/rum-events-format/pull/200 has different schema + // than `*.device` in common schema: https://github.com/DataDog/rum-events-format/blob/dcd62e58566b9d158c404f3588edc62c041262dd/schemas/rum/_common-schema.json#L264-L295 + // For that reason, we generate it under different name, so the `RUMTelemetryDevice` can be shared between telemetry events. + fixedName = "RUMTelemetryDevice" + } else { + fixedName = "RUMDevice" + } } if fixedName == "OS" { - fixedName = "RUMOperatingSystem" + if context.parent?.typeName == "telemetry" { + // The `telemetry.os` added in https://github.com/DataDog/rum-events-format/pull/200 has different schema + // than `*.os` in common schema: https://github.com/DataDog/rum-events-format/blob/dcd62e58566b9d158c404f3588edc62c041262dd/schemas/rum/_common-schema.json#L237-L262 + // For that reason, we generate it under different name, so the `RUMTelemetryOperatingSystem` can be shared between telemetry events. + fixedName = "RUMTelemetryOperatingSystem" + } else { + fixedName = "RUMOperatingSystem" + } } // Since https://github.com/DataDog/rum-events-format/pull/57 `action.id` can be either diff --git a/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift b/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift index efddd5b759..afcce8ae7b 100644 --- a/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift +++ b/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift @@ -209,6 +209,12 @@ internal class JSONSchema: Decodable { let schema = try reader.read(url) merge(with: schema) } + + // TODO: RUM-4571 [iOS] Generate RUM models for Usage Telemetry + // With addition of the "usage telemetry" the RUM schema was updated with unsupported constructs. + // To avoid `🚧 Unimplemented: "Building `SwiftAssociatedTypeEnum` case label for JSONObject is not supported` failure + // here we explicitly ignore the "usage" sub-schema. + oneOf = oneOf?.filter { $0.ref != "telemetry/usage-schema.json" } } // MARK: - Schemas Merging