diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 8784a404af..216c4dc51f 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -452,7 +452,7 @@ 61F74AF426F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */; }; 61F7F1DD266F9CB000F9F53B /* CodableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA02423979B00786299 /* CodableValue.swift */; }; 61F8CC092469295500FE2908 /* DatadogConfigurationBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F8CC082469295500FE2908 /* DatadogConfigurationBuilderTests.swift */; }; - 61F9CA712512450B000A5E61 /* RUMAutoInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA702512450B000A5E61 /* RUMAutoInstrumentationTests.swift */; }; + 61F9CA712512450B000A5E61 /* RUMInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA702512450B000A5E61 /* RUMInstrumentationTests.swift */; }; 61F9CA792512593A000A5E61 /* RUMNavigationControllerScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61F9CA782512593A000A5E61 /* RUMNavigationControllerScenario.storyboard */; }; 61F9CA8025125C01000A5E61 /* RUMNCSScreen3ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA7F25125C01000A5E61 /* RUMNCSScreen3ViewController.swift */; }; 61F9CA92251266F7000A5E61 /* RUMNavigationControllerScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA86251266CB000A5E61 /* RUMNavigationControllerScenarioTests.swift */; }; @@ -508,10 +508,18 @@ B3FC3C3C2653A97700DEED9E /* VitalInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */; }; D2135330270CA722000315AD /* DataCompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213532F270CA722000315AD /* DataCompressionTests.swift */; }; D22C1F5C271484B400922024 /* LogEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C1F5B271484B400922024 /* LogEventMapper.swift */; }; + D244B3A3271EDACD003E1B29 /* SwiftUIExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */; }; + D24985A22728048B00B4F72D /* SwiftUIRUMViewsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24985A12728048B00B4F72D /* SwiftUIRUMViewsHandler.swift */; }; + D24985A327280FD100B4F72D /* SwiftUIViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */; }; + D24985A727292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24985A627292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift */; }; D24C27EA270C8BEE005DE596 /* DataCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C27E9270C8BEE005DE596 /* DataCompression.swift */; }; + D2791EF927170A760046E07A /* RUMSwiftUIScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */; }; D2F1B81126D795F3009F3293 /* DDNoopRUMMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81026D795F3009F3293 /* DDNoopRUMMonitor.swift */; }; D2F1B81326D8DA68009F3293 /* DDNoopRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81226D8DA68009F3293 /* DDNoopRUMMonitorTests.swift */; }; D2F1B81526D8E5FF009F3293 /* DDNoopTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */; }; + D2F5BB36271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2F5BB35271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard */; }; + D2F5BB382718331800BDE2A4 /* SwiftUIRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F5BB372718331800BDE2A4 /* SwiftUIRootViewController.swift */; }; + D2FCA239271D896E0020286F /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */; }; E132727B24B333C700952F8B /* TracingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */; }; E132727D24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */; }; E13A880C257922EC004FB174 /* EnvironmentSpanIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13A880B257922EC004FB174 /* EnvironmentSpanIntegration.swift */; }; @@ -1102,7 +1110,7 @@ 61F3CDAC25122C9200C816E5 /* UIKitRUMViewsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsHandlerTests.swift; sourceTree = ""; }; 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugCrashReportingWithRUMViewController.swift; sourceTree = ""; }; 61F8CC082469295500FE2908 /* DatadogConfigurationBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogConfigurationBuilderTests.swift; sourceTree = ""; }; - 61F9CA702512450B000A5E61 /* RUMAutoInstrumentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMAutoInstrumentationTests.swift; sourceTree = ""; }; + 61F9CA702512450B000A5E61 /* RUMInstrumentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMInstrumentationTests.swift; sourceTree = ""; }; 61F9CA782512593A000A5E61 /* RUMNavigationControllerScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMNavigationControllerScenario.storyboard; sourceTree = ""; }; 61F9CA7F25125C01000A5E61 /* RUMNCSScreen3ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMNCSScreen3ViewController.swift; sourceTree = ""; }; 61F9CA86251266CB000A5E61 /* RUMNavigationControllerScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMNavigationControllerScenarioTests.swift; sourceTree = ""; }; @@ -1158,10 +1166,18 @@ B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoTests.swift; sourceTree = ""; }; D213532F270CA722000315AD /* DataCompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCompressionTests.swift; sourceTree = ""; }; D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = ""; }; + D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensionsTests.swift; sourceTree = ""; }; + D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIViewModifier.swift; sourceTree = ""; }; + D24985A12728048B00B4F72D /* SwiftUIRUMViewsHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIRUMViewsHandler.swift; sourceTree = ""; }; + D24985A627292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIRUMViewsHandlerTests.swift; sourceTree = ""; }; D24C27E9270C8BEE005DE596 /* DataCompression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCompression.swift; sourceTree = ""; }; + D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSwiftUIScenarioTests.swift; sourceTree = ""; }; D2F1B81026D795F3009F3293 /* DDNoopRUMMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopRUMMonitor.swift; sourceTree = ""; }; D2F1B81226D8DA68009F3293 /* DDNoopRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopRUMMonitorTests.swift; sourceTree = ""; }; D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopTracerTests.swift; sourceTree = ""; }; + D2F5BB35271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMSwiftUIInstrumentationScenario.storyboard; sourceTree = ""; }; + D2F5BB372718331800BDE2A4 /* SwiftUIRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIRootViewController.swift; sourceTree = ""; }; + D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = ""; }; E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingBenchmarkTests.swift; sourceTree = ""; }; E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingStorageBenchmarkTests.swift; sourceTree = ""; }; E13A880B257922EC004FB174 /* EnvironmentSpanIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentSpanIntegration.swift; sourceTree = ""; }; @@ -1485,6 +1501,7 @@ 61133BB82423979B00786299 /* InternalLoggers.swift */, 61133BBA2423979B00786299 /* SwiftExtensions.swift */, 615F197B25B5A64B00BE14B5 /* UIKitExtensions.swift */, + D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */, ); path = Utils; sourceTree = ""; @@ -1738,6 +1755,7 @@ 61133C362423990D00786299 /* InternalLoggersTests.swift */, 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */, + D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */, ); path = Utils; sourceTree = ""; @@ -1971,13 +1989,14 @@ 61337036250F84F100236D58 /* RUM */ = { isa = PBXGroup; children = ( - 9EC2835C26CFF56B00FACF1C /* MobileVitals */, 61D50C532580EF41006038A3 /* RUMScenarios.swift */, + 9EC2835C26CFF56B00FACF1C /* MobileVitals */, 61337037250F84FD00236D58 /* ManualInstrumentation */, 61F9CA7725125918000A5E61 /* NavigationControllerAutoInstrumentation */, 615AAC34251E34EF00C89EE9 /* TabBarAutoInstrumentation */, 6137E647252DD85400720485 /* ModalViewsAutoInstrumentation */, 6193DCA2251B5669009B8011 /* TapActionAutoInstrumentation */, + D2791EF32716F16E0046E07A /* SwiftUIInstrumentation */, 612D8F6725AEE65F000E2E09 /* Scrubbing */, ); path = RUM; @@ -2346,14 +2365,6 @@ path = URLSessionAutoInstrumentation; sourceTree = ""; }; - 616A9CD02535D36A00DB83CF /* UIKitHierarchyInspection */ = { - isa = PBXGroup; - children = ( - 616A9CD12535D38200DB83CF /* UIKitHierarchyInspectorTests.swift */, - ); - path = UIKitHierarchyInspection; - sourceTree = ""; - }; 616CCE11250A181C009FED46 /* Instrumentation */ = { isa = PBXGroup; children = ( @@ -2909,7 +2920,7 @@ 61FF281F24B89807000B3D9B /* RUMEvent */, 61FF282E24BC5E0E000B3D9B /* RUMEventOutputs */, 613E81F525A743470084B751 /* Scrubbing */, - 61F3CDA825121F8F00C816E5 /* AutoInstrumentation */, + 61F3CDA825121F8F00C816E5 /* Instrumentation */, 61786F7524FCDDE2009E6BAB /* Debugging */, 61411B0E24EC15940012EAB2 /* Utils */, ); @@ -3061,6 +3072,7 @@ 6164AF2D252C9C51000D78C4 /* RUMResourcesScenarioTests.swift */, 612D8F8025AF1C74000E2E09 /* RUMScrubbingScenarioTests.swift */, 9EC2835926CFEE0B00FACF1C /* RUMMobileVitalsScenarioTests.swift */, + D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */, ); path = RUM; sourceTree = ""; @@ -3069,28 +3081,27 @@ isa = PBXGroup; children = ( D249859B2727FF1D00B4F72D /* UIKit */, + D249859E2728042200B4F72D /* SwiftUI */, ); path = Views; sourceTree = ""; }; - 61F3CDA825121F8F00C816E5 /* AutoInstrumentation */ = { + 61F3CDA825121F8F00C816E5 /* Instrumentation */ = { isa = PBXGroup; children = ( - 61F9CA702512450B000A5E61 /* RUMAutoInstrumentationTests.swift */, + 61F9CA702512450B000A5E61 /* RUMInstrumentationTests.swift */, 61F3CDA925121FA100C816E5 /* Views */, 6141014C251A577D00E3C2D9 /* Actions */, 613F23EF252B1287006CD2D7 /* Resources */, ); - path = AutoInstrumentation; + path = Instrumentation; sourceTree = ""; }; 61F3CDA925121FA100C816E5 /* Views */ = { isa = PBXGroup; children = ( - 616A9CD02535D36A00DB83CF /* UIKitHierarchyInspection */, - 61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */, - 61F3CDAC25122C9200C816E5 /* UIKitRUMViewsHandlerTests.swift */, - 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */, + D24985A427292F5600B4F72D /* UIKit */, + D24985A527292F7300B4F72D /* SwiftUI */, ); path = Views; sourceTree = ""; @@ -3260,6 +3271,43 @@ path = UIKit; sourceTree = ""; }; + D249859E2728042200B4F72D /* SwiftUI */ = { + isa = PBXGroup; + children = ( + D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */, + D24985A12728048B00B4F72D /* SwiftUIRUMViewsHandler.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + D24985A427292F5600B4F72D /* UIKit */ = { + isa = PBXGroup; + children = ( + 61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */, + 61F3CDAC25122C9200C816E5 /* UIKitRUMViewsHandlerTests.swift */, + 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */, + 616A9CD12535D38200DB83CF /* UIKitHierarchyInspectorTests.swift */, + ); + path = UIKit; + sourceTree = ""; + }; + D24985A527292F7300B4F72D /* SwiftUI */ = { + isa = PBXGroup; + children = ( + D24985A627292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + D2791EF32716F16E0046E07A /* SwiftUIInstrumentation */ = { + isa = PBXGroup; + children = ( + D2F5BB35271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard */, + D2F5BB372718331800BDE2A4 /* SwiftUIRootViewController.swift */, + ); + path = SwiftUIInstrumentation; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3622,6 +3670,7 @@ files = ( 61337032250F82AE00236D58 /* LoggingManualInstrumentationScenario.storyboard in Resources */, 612D8F6925AEE68F000E2E09 /* RUMScrubbingScenario.storyboard in Resources */, + D2F5BB36271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard in Resources */, 611EA13C2580F77400BC0E56 /* TrackingConsentScenario.storyboard in Resources */, 61441C1124616DEC003D8BB8 /* LaunchScreen.storyboard in Resources */, 61337039250F852E00236D58 /* RUMManualInstrumentationScenario.storyboard in Resources */, @@ -3816,6 +3865,9 @@ files = ( 6112B10B25C849C000B37771 /* CrashReportingWithRUMIntegration.swift in Sources */, 61E917D12465423600E6C631 /* TracerConfiguration.swift in Sources */, + D24985A22728048B00B4F72D /* SwiftUIRUMViewsHandler.swift in Sources */, + D24985A327280FD100B4F72D /* SwiftUIViewModifier.swift in Sources */, + D2FCA239271D896E0020286F /* SwiftUIExtensions.swift in Sources */, 61C576C6256E65BD00295F7C /* DateCorrector.swift in Sources */, 61FC5F4525CC23C9006BB4DE /* RUMWithCrashContextIntegration.swift in Sources */, 61E909ED24A24DD3005EA2DE /* OTSpan.swift in Sources */, @@ -4118,10 +4170,11 @@ 6114FE3B25768AA90084E372 /* ConsentProviderTests.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */, + D244B3A3271EDACD003E1B29 /* SwiftUIExtensionsTests.swift in Sources */, 61F187FC25FA7DD60022CE9A /* InternalMonitoringFeatureTests.swift in Sources */, 61B5E42126DF85C7000B0A5F /* DDRUMMonitor+apiTests.m in Sources */, 61786F7724FCDE05009E6BAB /* RUMDebuggingTests.swift in Sources */, - 61F9CA712512450B000A5E61 /* RUMAutoInstrumentationTests.swift in Sources */, + 61F9CA712512450B000A5E61 /* RUMInstrumentationTests.swift in Sources */, 6156CB9324DDAA34008CB2B2 /* RUMCurrentContextTests.swift in Sources */, 61E45BE5245196EA00F2C652 /* SpanFileOutputTests.swift in Sources */, 61494CB524C864680082C633 /* RUMResourceScopeTests.swift in Sources */, @@ -4137,6 +4190,7 @@ 61F3CDAB25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift in Sources */, 9E989A4225F640D100235FC3 /* AppStateListenerTests.swift in Sources */, 617B954024BF4DB300E6F443 /* RUMApplicationScopeTests.swift in Sources */, + D24985A727292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift in Sources */, 61F2724925C943C500D54BF8 /* CrashReporterTests.swift in Sources */, 6172472725D673D7007085B3 /* CrashContextTests.swift in Sources */, 61BAD46A26415FCE001886CA /* OTSpanTests.swift in Sources */, @@ -4179,6 +4233,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D2F5BB382718331800BDE2A4 /* SwiftUIRootViewController.swift in Sources */, 618DCFE124C766F500589570 /* SendRUMFixture2ViewController.swift in Sources */, 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */, 61F9CA8025125C01000A5E61 /* RUMNCSScreen3ViewController.swift in Sources */, @@ -4230,6 +4285,7 @@ files = ( 61DC6D922539E3E300FFAA22 /* LoggingCommonAsserts.swift in Sources */, 61441C4024617013003D8BB8 /* IntegrationTests.swift in Sources */, + D2791EF927170A760046E07A /* RUMSwiftUIScenarioTests.swift in Sources */, 61163C4A252E03D6007DD5BF /* RUMModalViewsScenarioTests.swift in Sources */, 61B9ED212462089600C0DCFF /* TracingManualInstrumentationScenarioTests.swift in Sources */, 61B6811F25F0EA860015B4AF /* CrashReportingWithLoggingScenarioTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme index 0248c5a34c..562f97b28f 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -56,6 +56,11 @@ value = "LoggingManualInstrumentationScenario" isEnabled = "NO"> + + RUMView? { + if viewController is SwiftUIRootViewController { + return nil + } + + return `default`.rumView(for: viewController) + } + } + + func configureSDK(builder: Datadog.Configuration.Builder) { + _ = builder + .trackUIKitRUMViews(using: Predicate()) + .enableLogging(false) + .enableTracing(false) + } +} + // MARK: - Helpers private func rumResourceAttributesProvider( diff --git a/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/RUMSwiftUIInstrumentationScenario.storyboard b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/RUMSwiftUIInstrumentationScenario.storyboard new file mode 100644 index 0000000000..6eeb30c043 --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/RUMSwiftUIInstrumentationScenario.storyboard @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift new file mode 100644 index 0000000000..f213b89eb5 --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import SwiftUI +import Datadog + +/// A custom SwiftUI Hosting controller for `RootView`. +/// +/// This definition only exist to allow instantiation from `RUMSwiftUIInstrumentationScenario` +/// storyboard and should be ignored from RUM instrumentation. +@available(iOS 13, *) +class SwiftUIRootViewController: UIHostingController { + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: RootView()) + } +} + +/// The root view of the SwiftUI instrumentation test. +/// +/// This view creates a `SwiftUI.TabView` to present +/// navigation contexts.. +@available(iOS 13, *) +struct RootView: View { + var body: some View { + TabView { + tabNavigationView + .tabItem { + Text("Navigation View") + } + + tabScreenView + .tabItem { + Text("Screen 100") + } + } + } + + @ViewBuilder + var tabNavigationView: some View { + // An issue was introduced in iOS 14.2 (FB8907671) which makes + // `TabView` items to be loaded twice, once when the `TabView` + // appears` and once when the Tab item itself appears. This + // lead to RUM views being reported twice. This issue was fixed + // in iOS 14.5, see https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14_5-release-notes + // As a workaround, the tab view items can be embedded in a + // `LazyVStack` or `LazyHStack` to lazily load its content when + // it needs to render them onscreen. + if #available(iOS 14.5, *) { + NavigationView { + ScreenView(index: 1) + } + } else if #available(iOS 14.2, *) { + NavigationView { + LazyVStack { + ScreenView(index: 1) + } + } + } else { + NavigationView { + ScreenView(index: 1) + } + } + } + + @ViewBuilder + var tabScreenView: some View { + if #available(iOS 14.5, *) { + ScreenView(index: 100) + } else if #available(iOS 14.2, *) { + LazyVStack { + ScreenView(index: 100) + } + } else { + ScreenView(index: 100) + } + } +} + +/// A basic Screen View at a given index in the stack. +/// +/// This view presents a button to push a new view onto the +/// navigation stack, and a button to present a modal page sheet. +@available(iOS 13, *) +struct ScreenView: View { + + /// The view index in the stack. + let index: Int + + @State private var presentSheet = false + + var body: some View { + VStack(spacing: 32) { + NavigationLink("Push to Next View", destination: + ScreenView(index: index + 1) + ) + Button("Present Modal View") { + presentSheet.toggle() + } + } + .sheet(isPresented: $presentSheet) { + NavigationView { + destination + } + } + .navigationBarTitle("Screen \(index)") + .trackRUMView(name: "SwiftUI View \(index)") + } + + @ViewBuilder + var destination: some View { + ScreenView(index: index + 1) + } +} diff --git a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift index 33db7f2470..e461c7d0d5 100644 --- a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift +++ b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift @@ -48,6 +48,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { /// RUM Views auto instrumentation, `nil` if not enabled. let viewsAutoInstrumentation: ViewsAutoInstrumentation? + /// `SwiftUI.View` RUM instrumentation + let swiftUIViewInstrumentation: SwiftUIViewHandler /// RUM User Actions auto instrumentation, `nil` if not enabled. let userActionsAutoInstrumentation: UserActionsAutoInstrumentation? /// RUM Long Tasks auto instrumentation, `nil` if not enabled. @@ -84,6 +86,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { self.viewsAutoInstrumentation = viewsAutoInstrumentation self.userActionsAutoInstrumentation = userActionsAutoInstrumentation self.longTasks = longTasks + self.swiftUIViewInstrumentation = SwiftUIRUMViewsHandler(dateProvider: dateProvider) } func enable() { @@ -96,6 +99,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { viewsAutoInstrumentation?.handler.publish(to: subscriber) userActionsAutoInstrumentation?.handler.publish(to: subscriber) longTasks?.publish(to: subscriber) + swiftUIViewInstrumentation.publish(to: subscriber) } #if DD_SDK_COMPILED_FOR_TESTING diff --git a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift new file mode 100644 index 0000000000..7bc4cda09d --- /dev/null +++ b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift @@ -0,0 +1,195 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import UIKit + +/// Publisher generating RUM Commands on `SwiftUI.View` events. +internal protocol SwiftUIViewHandler: RUMCommandPublisher { + /// Respond to a `SwiftUI.View.onAppear` event. + func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) + + /// Respond to a `SwiftUI.View.onDisappear` event. + func onDisappear(identity: String) +} + +internal final class SwiftUIRUMViewsHandler: SwiftUIViewHandler { + /// RUM representation of a `SwiftUI.View`. + private struct View { + /// The RUM View identity. + let identity: String + + /// View name used for RUM Explorer. + let name: String + + /// View path used for RUM Explorer. + let path: String + + /// Custom attributes to attach to the View. + let attributes: [AttributeKey: AttributeValue] + } + + /// The current date provider. + private let dateProvider: DateProvider + + /// The notification center where this handler observe the following notifications: + /// - `UIApplicationDidEnterBackgroundNotification` + /// - `UIApplicationWillEnterForegroundNotification` + private weak var notificationCenter: NotificationCenter? + + /// The RUM Command subscriber responsible for processing + /// this publisher's commands. + private weak var subscriber: RUMCommandSubscriber? + + /// The appearing views stack. + /// + /// This stack allows to track appearing and disappearing views to consistently + /// publish start and stop commands to the subscriber. The last item of the + /// stack is the visible one, any items below it have appeared before but not yet + /// disappeared. Therefore, they are considered not visible but can be revealed + /// if the last item disappears. + private var stack: [View] = [] + + /// Creates a new `SwiftUI.View` handler to publish RUM view commands. + /// - Parameters: + /// - dateProvider: The current date provider. + /// - notificationCenter: The notification center where this handler + /// a set of `UIApplication` notifications. + init( + dateProvider: DateProvider, + notificationCenter: NotificationCenter = .default + ) { + self.dateProvider = dateProvider + self.notificationCenter = notificationCenter + + notificationCenter.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(applicationWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + deinit { + notificationCenter?.removeObserver( + self, + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + + notificationCenter?.removeObserver( + self, + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + func publish(to subscriber: RUMCommandSubscriber) { + self.subscriber = subscriber + } + + /// Respond to a `SwiftUI.View.onAppear` event. + /// + /// - Parameters: + /// - identity: The appearing `SwiftUI.View` identity. + /// - name: The appearing `SwiftUI.View` name. + /// - attributes: The appearing `SwiftUI.View` attributes. + func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) { + // Ignore the view if it's already visible + if stack.last?.identity == identity { + return + } + + // Stop the last appearing view of the stack + if let current = stack.last { + stop(view: current) + } + + let view = View( + identity: identity, + name: name, + path: path, + attributes: attributes + ) + + // Start the new appearing view + start(view: view) + // Add/Move the appearing view to the top + stack.removeAll(where: { $0.identity == identity }) + stack.append(view) + } + + /// Respond to a `SwiftUI.View.onDisappear` event. + /// + /// - Parameter identity: The disappearing `SwiftUI.View` identity. + func onDisappear(identity: String) { + guard stack.last?.identity == identity else { + // Remove any disappearing view from the stack if + // it's not visible. + return stack.removeAll(where: { $0.identity == identity }) + } + + // Stop and remove the visible view from the stack + let view = stack.removeLast() + stop(view: view) + + // Restart the previous view if any. + if let current = stack.last { + start(view: current) + } + } + + private func start(view: View) { + guard let subscriber = subscriber else { + return userLogger.warn( + """ + RUM View was started, but no `RUMMonitor` is registered on `Global.rum`. RUM instrumentation will not work. + Make sure `Global.rum = RUMMonitor.initialize()` is called before any `SwiftUI.View` appears. + """ + ) + } + + subscriber.process( + command: RUMStartViewCommand( + time: dateProvider.currentDate(), + identity: view.identity, + name: view.name, + path: view.path, + attributes: view.attributes + ) + ) + } + + private func stop(view: View) { + subscriber?.process( + command: RUMStopViewCommand( + time: dateProvider.currentDate(), + attributes: [:], + identity: view.identity + ) + ) + } + + @objc + private func applicationDidEnterBackground() { + if let current = stack.last { + stop(view: current) + } + } + + @objc + private func applicationWillEnterForeground() { + if let current = stack.last { + start(view: current) + } + } +} diff --git a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift new file mode 100644 index 0000000000..c6906dc601 --- /dev/null +++ b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift @@ -0,0 +1,62 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +#if canImport(SwiftUI) +import SwiftUI + +/// `SwiftUI.ViewModifier` for RUM which invoke `startView` and `stopView` from the +/// global RUM Monitor when the modified view appears and disappears. +@available(iOS 13, *) +internal struct RUMViewModifier: SwiftUI.ViewModifier { + /// The Content View identifier. + /// The id will be unique per modified view. + let identity: String = UUID().uuidString + + /// View Name used for RUM Explorer. + let name: String + + /// View Path used for RUM Explorer. + let path: String + + /// Custom attributes to attach to the View. + let attributes: [AttributeKey: AttributeValue] + + func body(content: Content) -> some View { + content.onAppear { + RUMInstrumentation.instance?.swiftUIViewInstrumentation + .onAppear( + identity: identity, + name: name, + path: path, + attributes: attributes + ) + } + .onDisappear { + RUMInstrumentation.instance?.swiftUIViewInstrumentation + .onDisappear(identity: identity) + } + } +} + +@available(iOS 13, *) +public extension SwiftUI.View { + /// Monitor this view with Datadog RUM. A start and stop events will be logged when this view appears + /// and disappears. + /// + /// - Parameters: + /// - name: the View name used for RUM Explorer. + /// - attributes: custom attributes to attach to the View. + /// - Returns: This view after applying a `ViewModifier` for monitoring the view. + func trackRUMView( + name: String, + attributes: [AttributeKey: AttributeValue] = [:] + ) -> some View { + let path = "\(name)/\(typeDescription.hashValue)" + return modifier(RUMViewModifier(name: name, path: path, attributes: attributes)) + } +} + +#endif diff --git a/Sources/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicate.swift b/Sources/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicate.swift index 94e4d93dd9..ed2c2be7cf 100644 --- a/Sources/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicate.swift +++ b/Sources/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicate.swift @@ -65,6 +65,14 @@ public struct DefaultUIKitRUMViewsPredicate: UIKitRUMViewsPredicate { return nil } + guard !isSwiftUI(class: type(of: viewController)) else { + // `SwiftUI` requires manual instrumentation in views. Therefore, all SwiftUI + // `UIKit` containers (e.g. `UIHostingController`) will be ignored from + // auto-intrumentation. + // This condition is wider and it ignores all view controllers defined in `SwiftUI` bundle. + return nil + } + let canonicalClassName = viewController.canonicalClassName var view = RUMView(name: canonicalClassName) view.path = canonicalClassName @@ -75,4 +83,9 @@ public struct DefaultUIKitRUMViewsPredicate: UIKitRUMViewsPredicate { private func isUIKit(`class`: AnyClass) -> Bool { return Bundle(for: `class`).isUIKit } + + /// If given `class` comes from SwiftUI framework. + private func isSwiftUI(`class`: AnyClass) -> Bool { + return Bundle(for: `class`).isSwiftUI + } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index fe71d769d9..a32c556d28 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -5,7 +5,6 @@ */ import Foundation -import class UIKit.UIViewController internal class RUMSessionScope: RUMScope, RUMContextProvider { struct Constants { diff --git a/Sources/Datadog/Utils/SwiftUIExtensions.swift b/Sources/Datadog/Utils/SwiftUIExtensions.swift new file mode 100644 index 0000000000..072fb22b57 --- /dev/null +++ b/Sources/Datadog/Utils/SwiftUIExtensions.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-2020 Datadog, Inc. + */ + +import Foundation + +#if canImport(SwiftUI) +import SwiftUI +#endif + +internal extension Bundle { + /// Returns `true` when `self` represents the `SwiftUI` framework bundle. + var isSwiftUI: Bool { + return bundleURL.lastPathComponent == "SwiftUI.framework" + } +} + +#if canImport(SwiftUI) +@available(iOS 13, *) +internal extension SwiftUI.View { + /// The Type descriptionof this view. + var typeDescription: String { + return String(describing: type(of: self)) + } +} +#endif diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMNavigationControllerScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMNavigationControllerScenarioTests.swift index 941b190c46..97a13f2d86 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMNavigationControllerScenarioTests.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMNavigationControllerScenarioTests.swift @@ -9,20 +9,20 @@ import XCTest private extension ExampleApplication { func tapPushNextScreenButton() { - buttons["Push Next Screen"].tap() + buttons["Push Next Screen"].safeTap(within: 5) } func tapBackButton() { - navigationBars["Screen 4"].buttons["Screen 3"].tap() + navigationBars["Screen 4"].buttons["Screen 3"].safeTap() } func tapPopToTheFirstScreenButton() { - buttons["Pop To The First Screen"].tap() + buttons["Pop To The First Screen"].safeTap() } func swipeInteractiveBackGesture() { let coordinate1 = coordinate(withNormalizedOffset: .init(dx: 0, dy: 0.5)) - let coordinate2 = coordinate(withNormalizedOffset: .init(dx: 0.75, dy: 0.5)) + let coordinate2 = coordinate(withNormalizedOffset: .init(dx: 0.80, dy: 0.5)) coordinate1.press(forDuration: 0.5, thenDragTo: coordinate2) } } diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift new file mode 100644 index 0000000000..8931e01296 --- /dev/null +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift @@ -0,0 +1,113 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest + +private extension ExampleApplication { + func tapTapBar(item name: String) { + tabBars.buttons[name].safeTap() + } + + func tapPushToNextView() { + buttons["Push to Next View"].safeTap(within: 10) + } + + func tapPresentModalView() { + buttons["Present Modal View"].safeTap(within: 10) + } + + func tapBackButton(to index: Int) { + buttons["Screen \(index)"].safeTap(within: 10) + } + + func swipeRightInteraction() { + let coordinate1 = coordinate(withNormalizedOffset: .init(dx: 0, dy: 0.5)) + let coordinate2 = coordinate(withNormalizedOffset: .init(dx: 0.80, dy: 0.5)) + coordinate1.press(forDuration: 0.5, thenDragTo: coordinate2) + } + + func swipeDownInteraction() { + let coordinate1 = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.2)) + let coordinate2 = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.8)) + coordinate1.press(forDuration: 0.5, thenDragTo: coordinate2) + } +} + +class RUMSwiftUIScenarioTests: IntegrationTests, RUMCommonAsserts { + func testSwiftUIScenario() throws { + guard #available(iOS 13, *) else { + return + } + + // Server session recording RUM events send to `HTTPServerMock`. + let recording = server.obtainUniqueRecordingSession() + + let app = ExampleApplication() + app.launchWith( + testScenarioClassName: "RUMSwiftUIInstrumentationScenario", + serverConfiguration: HTTPServerMockConfiguration( + rumEndpoint: recording.recordingURL + ) + ) + + // start on "SwiftUI.View 1" + app.tapPushToNextView() // go to "SwiftUI.View 2" + app.tapPushToNextView() // go to "SwiftUI.View 3" + app.tapPresentModalView() // go to "SwiftUI.View 4" + app.swipeDownInteraction() // go to "SwiftUI.View 3" + app.tapTapBar(item: "Screen 100") // go to "SwiftUI.View 100" + app.tapPresentModalView() // go to "SwiftUI.View 101" + app.swipeDownInteraction() // go back to "SwiftUI.View 100" + app.tapTapBar(item: "Navigation View") // go to "SwiftUI.View 3" + + // Get RUM Sessions with expected number of View visits + let requests = try recording.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + try RUMSessionMatcher.singleSession(from: requests)?.viewVisits.count == 9 + } + + assertRUM(requests: requests) + + let session = try XCTUnwrap(RUMSessionMatcher.singleSession(from: requests)) + let visits = session.viewVisits + + XCTAssertEqual(visits[0].name, "SwiftUI View 1") + XCTAssertTrue(visits[0].path.matches(regex: "SwiftUI View 1\\/[0-9]*")) + XCTAssertEqual(visits[0].actionEvents[0].action.type, .applicationStart) + XCTAssertGreaterThan(visits[0].actionEvents[0].action.loadingTime!, 0) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[0]) // go to "Screen 2" + + XCTAssertEqual(visits[1].name, "SwiftUI View 2") + XCTAssertTrue(visits[1].path.matches(regex: "SwiftUI View 2\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[1])// go to "Screen 3" + + XCTAssertEqual(visits[2].name, "SwiftUI View 3") + XCTAssertTrue(visits[2].path.matches(regex: "SwiftUI View 3\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[2])// go to "Screen 4" + + XCTAssertEqual(visits[3].name, "SwiftUI View 4") + XCTAssertTrue(visits[3].path.matches(regex: "SwiftUI View 4\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[3])// go to "Screen 3" + + XCTAssertEqual(visits[4].name, "SwiftUI View 3") + XCTAssertTrue(visits[4].path.matches(regex: "SwiftUI View 3\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[4])// go to "Screen 100" + + XCTAssertEqual(visits[5].name, "SwiftUI View 100") + XCTAssertTrue(visits[5].path.matches(regex: "SwiftUI View 100\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[5])// go to "Screen 101" + + XCTAssertEqual(visits[6].name, "SwiftUI View 101") + XCTAssertTrue(visits[6].path.matches(regex: "SwiftUI View 101\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[6])// go to "Screen 100" + + XCTAssertEqual(visits[7].name, "SwiftUI View 100") + XCTAssertTrue(visits[7].path.matches(regex: "SwiftUI View 100\\/[0-9]*")) + RUMSessionMatcher.assertViewWasEventuallyInactive(visits[7])// go to "Screen 3" + + XCTAssertEqual(visits[8].name, "SwiftUI View 3") + XCTAssertTrue(visits[8].path.matches(regex: "SwiftUI View 3\\/[0-9]*")) + } +} diff --git a/Tests/DatadogIntegrationTests/UITestsHelpers.swift b/Tests/DatadogIntegrationTests/UITestsHelpers.swift index 1c3c9dbf1f..5b15f3d5e8 100644 --- a/Tests/DatadogIntegrationTests/UITestsHelpers.swift +++ b/Tests/DatadogIntegrationTests/UITestsHelpers.swift @@ -78,3 +78,11 @@ extension String { struct Exception: Error, CustomStringConvertible { let description: String } + +extension XCUIElement { + func safeTap(within timeout: TimeInterval = 0) { + if waitForExistence(timeout: timeout) && isHittable { + tap() + } + } +} diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Actions/UIApplicationSwizzlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Actions/UIApplicationSwizzlerTests.swift similarity index 100% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Actions/UIApplicationSwizzlerTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Actions/UIApplicationSwizzlerTests.swift diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Actions/UIKitRUMUserActionsHandlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Actions/UIKitRUMUserActionsHandlerTests.swift similarity index 100% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Actions/UIKitRUMUserActionsHandlerTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Actions/UIKitRUMUserActionsHandlerTests.swift diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/RUMAutoInstrumentationTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/RUMInstrumentationTests.swift similarity index 98% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/RUMAutoInstrumentationTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/RUMInstrumentationTests.swift index 8cd4de0edd..a543e5f070 100644 --- a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/RUMAutoInstrumentationTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/Instrumentation/RUMInstrumentationTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import Datadog -class RUMAutoInstrumentationTests: XCTestCase { +class RUMInstrumentationTests: XCTestCase { override func setUp() { super.setUp() XCTAssertNil(RUMFeature.instance) diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift similarity index 100% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift diff --git a/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift new file mode 100644 index 0000000000..f7a321ace9 --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift @@ -0,0 +1,304 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class SwiftUIRUMViewsHandlerTests: XCTestCase { + private let dateProvider = RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) + private let commandSubscriber = RUMCommandSubscriberMock() + private let notificationCenter = NotificationCenter() + + private lazy var handler: SwiftUIRUMViewsHandler = { + let handler = SwiftUIRUMViewsHandler( + dateProvider: dateProvider, + notificationCenter: notificationCenter + ) + handler.publish(to: commandSubscriber) + return handler + }() + + // MARK: - Handling `.onAppear` + + func testWhenOnAppear_itStartsRUMView() throws { + // Given + let viewIdentity: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewIdentity, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 1) + + let command = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) + XCTAssertEqual(command.time, .mockDecember15th2019At10AMUTC()) + XCTAssertTrue(command.identity.equals(viewIdentity)) + XCTAssertEqual(command.name, viewName) + XCTAssertEqual(command.path, viewPath) + AssertDictionariesEqual(command.attributes, viewAttributes) + } + + func testWhenOnAppear_itStopsPreviousRUMView() throws { + // Given + let view1Identity: String = UUID().uuidString + let view1Name: String = .mockRandom() + let view1Path: String = .mockRandom() + let view1Attributes = mockRandomAttributes() + + let view2Identity: String = UUID().uuidString + let view2Name: String = .mockRandom() + let view2Path: String = .mockRandom() + let view2Attributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: view1Identity, + name: view1Name, + path: view1Path, + attributes: view1Attributes + ) + + handler.onAppear( + identity: view2Identity, + name: view2Name, + path: view2Path, + attributes: view2Attributes + ) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 3) + + let startCommand1 = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) + let stopCommand = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) + let startCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) + + XCTAssertTrue(startCommand1.identity.equals(view1Identity)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + XCTAssertTrue(stopCommand.identity.equals(view1Identity)) + XCTAssertEqual(stopCommand.attributes.count, 0) + XCTAssertTrue(startCommand2.identity.equals(view2Identity)) + AssertDictionariesEqual(startCommand2.attributes, view2Attributes) + } + + func testWhenOnAppear_itDoesNotStartTheSameRUMViewTwice() throws { + // Given + let viewIdentity: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewIdentity, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + handler.onAppear( + identity: viewIdentity, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 1) + XCTAssertTrue(commandSubscriber.receivedCommands[0] is RUMStartViewCommand) + } + + // MARK: - Handling `onDisappear` + + func testWhenOnDisappear_itDoesNotSendAnyCommand() { + // Given + let viewIdentity: String = UUID().uuidString + + // When + handler.onDisappear(identity: viewIdentity) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 0) + } + + func testGivenAppearedView_whenOnDisappear_itSopsTheRUMView() throws { + // Given + let viewIdentity: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewIdentity, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + handler.onDisappear(identity: viewIdentity) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 2) + + let startCommand = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) + let stopCommand = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) + + XCTAssertTrue(startCommand.identity.equals(viewIdentity)) + AssertDictionariesEqual(startCommand.attributes, viewAttributes) + XCTAssertTrue(stopCommand.identity.equals(viewIdentity)) + XCTAssertEqual(stopCommand.attributes.count, 0) + } + + func testGiven2AppearedView_whenTheFirstDisappears_itDoesNotStopItTwice() throws { + // Given + let view1Identity: String = UUID().uuidString + let view1Name: String = .mockRandom() + let view1Path: String = .mockRandom() + let view1Attributes = mockRandomAttributes() + + let view2Identity: String = UUID().uuidString + let view2Name: String = .mockRandom() + let view2Path: String = .mockRandom() + let view2Attributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: view1Identity, + name: view1Name, + path: view1Path, + attributes: view1Attributes + ) + + handler.onAppear( + identity: view2Identity, + name: view2Name, + path: view2Path, + attributes: view2Attributes + ) + + handler.onDisappear(identity: view1Identity) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 3) + + let startCommand1 = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) + let stopCommand1 = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) + let startCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) + + XCTAssertTrue(startCommand1.identity.equals(view1Identity)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + XCTAssertTrue(stopCommand1.identity.equals(view1Identity)) + XCTAssertTrue(startCommand2.identity.equals(view2Identity)) + AssertDictionariesEqual(startCommand2.attributes, view2Attributes) + } + + func testGiven2AppearedView_whenTheLastDisappears_itRestartsThePreviousRUMView() throws { + // Given + let view1Identity: String = UUID().uuidString + let view1Name: String = .mockRandom() + let view1Path: String = .mockRandom() + let view1Attributes = mockRandomAttributes() + + let view2Identity: String = UUID().uuidString + let view2Name: String = .mockRandom() + let view2Path: String = .mockRandom() + let view2Attributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: view1Identity, + name: view1Name, + path: view1Path, + attributes: view1Attributes + ) + + handler.onAppear( + identity: view2Identity, + name: view2Name, + path: view2Path, + attributes: view2Attributes + ) + + handler.onDisappear(identity: view2Identity) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 5) + + let startCommand1 = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) + let stopCommand1 = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) + let startCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) + let stopCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[3] as? RUMStopViewCommand) + let startCommand3 = try XCTUnwrap(commandSubscriber.receivedCommands[4] as? RUMStartViewCommand) + + XCTAssertTrue(startCommand1.identity.equals(view1Identity)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + XCTAssertTrue(stopCommand1.identity.equals(view1Identity)) + XCTAssertTrue(startCommand2.identity.equals(view2Identity)) + AssertDictionariesEqual(startCommand2.attributes, view2Attributes) + XCTAssertTrue(stopCommand2.identity.equals(view2Identity)) + XCTAssertTrue(startCommand3.identity.equals(view1Identity)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + } + + // MARK: - Handling Application Activity + + func testGivenRUMViewStarted_whenAppStateChanges_itStopsAndRestartsRUMView() throws { + // Given + let viewIdentity: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewIdentity, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + dateProvider.advance(bySeconds: 1) + notificationCenter.post(name: UIApplication.willEnterForegroundNotification, object: nil) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 3) + + let stopCommand = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) + let startCommand = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) + XCTAssertTrue(stopCommand.identity.equals(viewIdentity)) + XCTAssertEqual(stopCommand.attributes.count, 0) + XCTAssertEqual(stopCommand.time, .mockDecember15th2019At10AMUTC()) + XCTAssertTrue(startCommand.identity.equals(viewIdentity)) + XCTAssertEqual(startCommand.path, viewPath) + XCTAssertEqual(startCommand.name, viewName) + AssertDictionariesEqual(startCommand.attributes, viewAttributes) + XCTAssertEqual(startCommand.time, .mockDecember15th2019At10AMUTC() + 1) + } + + func testGivenRUMViewDidNotStart_whenAppStateChanges_itDoesNothing() throws { + // Given + let viewIdentity: String = UUID().uuidString + + // When + handler.onDisappear(identity: viewIdentity) + + notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) + dateProvider.advance(bySeconds: 1) + notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 0) + } +} diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitHierarchyInspection/UIKitHierarchyInspectorTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitHierarchyInspectorTests.swift similarity index 100% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitHierarchyInspection/UIKitHierarchyInspectorTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitHierarchyInspectorTests.swift diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitRUMViewsHandlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsHandlerTests.swift similarity index 100% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitRUMViewsHandlerTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsHandlerTests.swift diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitRUMViewsPredicateTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicateTests.swift similarity index 76% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitRUMViewsPredicateTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicateTests.swift index ced5ee0ad7..450101e1d1 100644 --- a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIKitRUMViewsPredicateTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIKitRUMViewsPredicateTests.swift @@ -7,6 +7,10 @@ import XCTest import Datadog +#if canImport(SwiftUI) +import SwiftUI +#endif + class UIKitRUMViewsPredicateTests: XCTestCase { func testGivenDefaultPredicate_whenAskingForCustomSwiftViewController_itNamesTheViewByItsClassName() { // Given @@ -47,4 +51,21 @@ class UIKitRUMViewsPredicateTests: XCTestCase { // Then XCTAssertNil(rumView) } + +#if canImport(SwiftUI) + func testGivenDefaultPredicate_whenAskingSwiftUIViewController_itReturnsNoView() { + guard #available(iOS 13, *) else { + return + } + // Given + let predicate = DefaultUIKitRUMViewsPredicate() + + // When + let swiftUIHostingController = UIHostingController(rootView: EmptyView()) + let rumView = predicate.rumView(for: swiftUIHostingController) + + // Then + XCTAssertNil(rumView) + } +#endif } diff --git a/Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIViewControllerSwizzlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIViewControllerSwizzlerTests.swift similarity index 100% rename from Tests/DatadogTests/Datadog/RUM/AutoInstrumentation/Views/UIViewControllerSwizzlerTests.swift rename to Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/UIKit/UIViewControllerSwizzlerTests.swift diff --git a/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift b/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift new file mode 100644 index 0000000000..cdc6f2d342 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift @@ -0,0 +1,52 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +#if canImport(SwiftUI) + +import XCTest +import SwiftUI +@testable import Datadog + +@available(iOS 13, *) +class CustomHostingController: UIHostingController {} + +@available(iOS 13, *) +final class TestView: View { + var body = EmptyView() +} + +class SwiftUIExtensionsTests: XCTestCase { + func testSwiftUIViewTypeDescription() { + guard #available(iOS 13, *) else { + return + } + + let view = TestView().cornerRadius(8) + XCTAssertEqual(view.typeDescription, "ModifiedContent>") + } + + func testBundleIsSwiftUI() { + guard #available(iOS 13, *) else { + return + } + + // Given + let someSwiftUITypes: [AnyClass] = [ + UIHostingController.self // The only class in SwiftUI + ] + + let someNonSwiftUITypes: [AnyClass] = [ + TestView.self, + UIViewController.self, + OperationQueue.self, + ] + + // Then + someSwiftUITypes.forEach { XCTAssertTrue(Bundle(for: $0).isSwiftUI) } + someNonSwiftUITypes.forEach { XCTAssertFalse(Bundle(for: $0).isSwiftUI) } + } +} +#endif diff --git a/api-surface-swift b/api-surface-swift index 5ae7da6003..45afc78ca3 100644 --- a/api-surface-swift +++ b/api-surface-swift @@ -326,6 +326,8 @@ public protocol UIKitRUMUserActionsPredicate public struct DefaultUIKitRUMUserActionsPredicate: UIKitRUMUserActionsPredicate public init () public func rumAction(targetView: UIView) -> RUMAction? +public extension SwiftUI.View + func trackRUMView(name: String,attributes: [AttributeKey: AttributeValue] = [:]) -> some View public struct RUMView public var name: String public var path: String?