From e13213dfece23b7279b55bba03ba058e97e8f23d Mon Sep 17 00:00:00 2001 From: maxep Date: Fri, 22 Oct 2021 17:00:23 +0200 Subject: [PATCH 1/8] RUMM-1615 SwiftUI View Instrumentation --- Datadog/Datadog.xcodeproj/project.pbxproj | 96 ++++-- .../xcshareddata/xcschemes/Example.xcscheme | 5 + .../Example/Scenarios/RUM/RUMScenarios.swift | 26 ++ ...MSwiftUIInstrumentationScenario.storyboard | 33 ++ .../SwiftUIRootViewController.swift | 78 +++++ .../Instrumentation/RUMInstrumentation.swift | 14 + .../SwiftUI/SwiftUIRUMViewsHandler.swift | 195 +++++++++++ .../Views/SwiftUI/SwiftUIViewModifier.swift | 60 ++++ .../Views/UIKit/UIKitRUMViewsPredicate.swift | 13 + .../RUMMonitor/Scopes/RUMSessionScope.swift | 1 - .../RUMMonitor/Scopes/RUMViewIdentity.swift | 4 + Sources/Datadog/Utils/SwiftUIExtensions.swift | 28 ++ ...RUMNavigationControllerScenarioTests.swift | 8 +- .../RUM/RUMSwiftUIScenarioTests.swift | 113 +++++++ .../UITestsHelpers.swift | 8 + .../Actions/UIApplicationSwizzlerTests.swift | 0 .../UIKitRUMUserActionsHandlerTests.swift | 0 .../RUMInstrumentationTests.swift} | 2 +- .../URLSessionRUMResourcesHandlerTests.swift | 0 .../SwiftUI/SwiftUIRUMViewsHandlerTests.swift | 304 ++++++++++++++++++ .../UIKit}/UIKitHierarchyInspectorTests.swift | 0 .../UIKit}/UIKitRUMViewsHandlerTests.swift | 0 .../UIKit}/UIKitRUMViewsPredicateTests.swift | 21 ++ .../UIViewControllerSwizzlerTests.swift | 0 .../Utils/SwiftUIExtensionsTests.swift | 52 +++ api-surface-swift | 2 + 26 files changed, 1037 insertions(+), 26 deletions(-) create mode 100644 Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/RUMSwiftUIInstrumentationScenario.storyboard create mode 100644 Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift create mode 100644 Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift create mode 100644 Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift create mode 100644 Sources/Datadog/Utils/SwiftUIExtensions.swift create mode 100644 Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation => Instrumentation}/Actions/UIApplicationSwizzlerTests.swift (100%) rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation => Instrumentation}/Actions/UIKitRUMUserActionsHandlerTests.swift (100%) rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation/RUMAutoInstrumentationTests.swift => Instrumentation/RUMInstrumentationTests.swift} (98%) rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation => Instrumentation}/Resources/URLSessionRUMResourcesHandlerTests.swift (100%) create mode 100644 Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation/Views/UIKitHierarchyInspection => Instrumentation/Views/UIKit}/UIKitHierarchyInspectorTests.swift (100%) rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation/Views => Instrumentation/Views/UIKit}/UIKitRUMViewsHandlerTests.swift (100%) rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation/Views => Instrumentation/Views/UIKit}/UIKitRUMViewsPredicateTests.swift (76%) rename Tests/DatadogTests/Datadog/RUM/{AutoInstrumentation/Views => Instrumentation/Views/UIKit}/UIViewControllerSwizzlerTests.swift (100%) create mode 100644 Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift 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..269a3dfb23 --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift @@ -0,0 +1,78 @@ +/* + * 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 + +@available(iOS 13, *) +/// A custom SwiftUI Hosting controller for `RootView`. +/// +/// This definition only exist to allow instantiation from `RUMSwiftUIInstrumentationScenario` +/// storyboard and should be ignored from RUM instrumentation. +class SwiftUIRootViewController: UIHostingController { + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: RootView()) + } +} + +@available(iOS 13, *) +/// The root view of the SwiftUI instrumentation test. +/// +/// This view creates a navigation stack and present a`ScreenView` as fist view. +struct RootView: View { + var body: some View { + TabView { + NavigationView { + ScreenView(index: 1) + }.tabItem { + Text("Navigation View") + } + + ScreenView(index: 100) + .tabItem { + Text("Screen 100") + } + } + } +} + + +@available(iOS 13, *) +/// A basic Screen View at a given index in the stack. +/// +/// This view presents a single navigation button to push a +/// `UIScreenView` onto the stack. +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..709dee16b0 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 @@ -107,3 +111,13 @@ internal final class RUMInstrumentation: RUMCommandPublisher { } #endif } + +extension RUMInstrumentation: SwiftUIViewHandler { + func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) { + swiftUIViewInstrumentation.onAppear(identity: identity, name: name, path: path, attributes: attributes) + } + + func onDisappear(identity: String) { + swiftUIViewInstrumentation.onDisappear(identity: identity) + } +} 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..070deb7e81 --- /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: + /// - key: The appearing `SwiftUI.View` key. + /// - 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 key: The disappearing `SwiftUI.View` key. + 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..a8a6bbbeca --- /dev/null +++ b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-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 id: 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?.onAppear( + identity: id, + name: name, + path: path, + attributes: attributes + ) + } + .onDisappear { + RUMInstrumentation.instance?.onDisappear(identity: id) + } + } +} + +@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/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift index 55d240095e..6c9e2ca6bf 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift @@ -63,6 +63,10 @@ extension String: RUMViewIdentifiable { var defaultViewPath: String { self } } +internal func == (lhs: RUMViewIdentifiable?, rhs: RUMViewIdentifiable) -> Bool { + return lhs?.equals(rhs) ?? false +} + // MARK: - `RUMViewIdentity` /// Manages the `RUMViewIdentifiable` by using either reference or value semantic. 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..069a5e51e5 --- /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.1)) + 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..6b6ca6b355 --- /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 viewKey: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewKey, + 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(viewKey)) + XCTAssertEqual(command.name, viewName) + XCTAssertEqual(command.path, viewPath) + AssertDictionariesEqual(command.attributes, viewAttributes) + } + + func testWhenOnAppear_itStopsPreviousRUMView() throws { + // Given + let view1Key: String = UUID().uuidString + let view1Name: String = .mockRandom() + let view1Path: String = .mockRandom() + let view1Attributes = mockRandomAttributes() + + let view2Key: String = UUID().uuidString + let view2Name: String = .mockRandom() + let view2Path: String = .mockRandom() + let view2Attributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: view1Key, + name: view1Name, + path: view1Path, + attributes: view1Attributes + ) + + handler.onAppear( + identity: view2Key, + 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(view1Key)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + XCTAssertTrue(stopCommand.identity.equals(view1Key)) + XCTAssertEqual(stopCommand.attributes.count, 0) + XCTAssertTrue(startCommand2.identity.equals(view2Key)) + AssertDictionariesEqual(startCommand2.attributes, view2Attributes) + } + + func testWhenOnAppear_itDoesNotStartTheSameRUMViewTwice() throws { + // Given + let viewKey: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewKey, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + handler.onAppear( + identity: viewKey, + 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 viewKey: String = UUID().uuidString + + // When + handler.onDisappear(identity: viewKey) + + // Then + XCTAssertEqual(commandSubscriber.receivedCommands.count, 0) + } + + func testGivenAppearedView_whenOnDisappear_itSopsTheRUMView() throws { + // Given + let viewKey: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewKey, + name: viewName, + path: viewPath, + attributes: viewAttributes + ) + + handler.onDisappear(identity: viewKey) + + // 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(viewKey)) + AssertDictionariesEqual(startCommand.attributes, viewAttributes) + XCTAssertTrue(stopCommand.identity.equals(viewKey)) + XCTAssertEqual(stopCommand.attributes.count, 0) + } + + func testGiven2AppearedView_whenTheFirstDisappears_itDoesNotStopItTwice() throws { + // Given + let view1Key: String = UUID().uuidString + let view1Name: String = .mockRandom() + let view1Path: String = .mockRandom() + let view1Attributes = mockRandomAttributes() + + let view2Key: String = UUID().uuidString + let view2Name: String = .mockRandom() + let view2Path: String = .mockRandom() + let view2Attributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: view1Key, + name: view1Name, + path: view1Path, + attributes: view1Attributes + ) + + handler.onAppear( + identity: view2Key, + name: view2Name, + path: view2Path, + attributes: view2Attributes + ) + + handler.onDisappear(identity: view1Key) + + // 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(view1Key)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + XCTAssertTrue(stopCommand1.identity.equals(view1Key)) + XCTAssertTrue(startCommand2.identity.equals(view2Key)) + AssertDictionariesEqual(startCommand2.attributes, view2Attributes) + } + + func testGiven2AppearedView_whenTheLastDisappears_itRestartsThePreviousRUMView() throws { + // Given + let view1Key: String = UUID().uuidString + let view1Name: String = .mockRandom() + let view1Path: String = .mockRandom() + let view1Attributes = mockRandomAttributes() + + let view2Key: String = UUID().uuidString + let view2Name: String = .mockRandom() + let view2Path: String = .mockRandom() + let view2Attributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: view1Key, + name: view1Name, + path: view1Path, + attributes: view1Attributes + ) + + handler.onAppear( + identity: view2Key, + name: view2Name, + path: view2Path, + attributes: view2Attributes + ) + + handler.onDisappear(identity: view2Key) + + // 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(view1Key)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + XCTAssertTrue(stopCommand1.identity.equals(view1Key)) + XCTAssertTrue(startCommand2.identity.equals(view2Key)) + AssertDictionariesEqual(startCommand2.attributes, view2Attributes) + XCTAssertTrue(stopCommand2.identity.equals(view2Key)) + XCTAssertTrue(startCommand3.identity.equals(view1Key)) + AssertDictionariesEqual(startCommand1.attributes, view1Attributes) + } + + // MARK: - Handling Application Activity + + func testGivenRUMViewStarted_whenAppStateChanges_itStopsAndRestartsRUMView() throws { + // Given + let viewKey: String = UUID().uuidString + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + + // When + handler.onAppear( + identity: viewKey, + 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(viewKey)) + XCTAssertEqual(stopCommand.attributes.count, 0) + XCTAssertEqual(stopCommand.time, .mockDecember15th2019At10AMUTC()) + XCTAssertTrue(startCommand.identity.equals(viewKey)) + XCTAssertEqual(startCommand.path, viewPath) + XCTAssertEqual(startCommand.name, viewName) + AssertDictionariesEqual(startCommand.attributes, viewAttributes) + XCTAssertEqual(startCommand.time, .mockDecember15th2019At10AMUTC() + 1) + } + + func testGivenRUMViewDidNotStart_whenAppStateChanges_itDoesNothing() throws { + // Given + let viewKey: String = UUID().uuidString + + // When + handler.onDisappear(identity: viewKey) + + 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..fc54b76e04 --- /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().trackRUMView(name: "test") + 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? From c4a79567e77b0bf9b93f8ebebf07b9ae63dcd8d3 Mon Sep 17 00:00:00 2001 From: maxep Date: Wed, 27 Oct 2021 15:12:23 +0200 Subject: [PATCH 2/8] RUMM-1615 Fix integration test flakiness --- .../Scenarios/RUM/RUMSwiftUIScenarioTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift index 069a5e51e5..8931e01296 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift @@ -30,7 +30,7 @@ private extension ExampleApplication { } func swipeDownInteraction() { - let coordinate1 = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.1)) + 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) } From 566a95b67be77197f90b6a03357f73ff7d79219a Mon Sep 17 00:00:00 2001 From: maxep Date: Thu, 28 Oct 2021 12:17:52 +0200 Subject: [PATCH 3/8] RUMM-1615 Workaround FB8907671 in 14.2 <= iOS < 14.5 --- .../SwiftUIRootViewController.swift | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift index 269a3dfb23..c8627c637e 100644 --- a/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift +++ b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift @@ -26,21 +26,59 @@ class SwiftUIRootViewController: UIHostingController { 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) - }.tabItem { - Text("Navigation View") } + } 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) - .tabItem { - Text("Screen 100") + } else if #available(iOS 14.2, *) { + LazyVStack { + ScreenView(index: 100) } + } else { + ScreenView(index: 100) } } } - @available(iOS 13, *) /// A basic Screen View at a given index in the stack. /// From ea8193658d29086d06b23d6c40956f0174f68040 Mon Sep 17 00:00:00 2001 From: maxep Date: Wed, 3 Nov 2021 17:18:57 +0100 Subject: [PATCH 4/8] RUMM-1615 Add `RUMViewModifier` unit tests --- Datadog/Datadog.xcodeproj/project.pbxproj | 4 + .../Instrumentation/RUMInstrumentation.swift | 18 +++- .../Datadog/Mocks/RUMFeatureMocks.swift | 18 ++++ .../Views/SwiftUI/RUMViewModifierTests.swift | 94 +++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 216c4dc51f..6cea2c91b8 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -514,6 +514,7 @@ 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 */; }; + D2EFF3D52732D48800D09F33 /* RUMViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFF3D42732D48800D09F33 /* RUMViewModifierTests.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 */; }; @@ -1172,6 +1173,7 @@ 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 = ""; }; + D2EFF3D42732D48800D09F33 /* RUMViewModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewModifierTests.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 = ""; }; @@ -3295,6 +3297,7 @@ isa = PBXGroup; children = ( D24985A627292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift */, + D2EFF3D42732D48800D09F33 /* RUMViewModifierTests.swift */, ); path = SwiftUI; sourceTree = ""; @@ -4061,6 +4064,7 @@ 61D980BC24E293F600E03345 /* RUMIntegrationsTests.swift in Sources */, 61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */, 61411B1024EC15AC0012EAB2 /* Casting+RUM.swift in Sources */, + D2EFF3D52732D48800D09F33 /* RUMViewModifierTests.swift in Sources */, 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */, 61FF283024BC5E2D000B3D9B /* RUMEventFileOutputTests.swift in Sources */, 9E986C302677B91400D62490 /* VitalRefreshRateReaderTests.swift in Sources */, diff --git a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift index 709dee16b0..cb1b90c0e9 100644 --- a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift +++ b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift @@ -57,7 +57,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { // MARK: - Initialization - init( + convenience init( configuration: FeaturesConfiguration.RUM.Instrumentation, dateProvider: DateProvider ) { @@ -83,10 +83,24 @@ internal final class RUMInstrumentation: RUMCommandPublisher { longTasks = LongTaskObserver(threshold: threshold, dateProvider: dateProvider) } + self.init( + viewsAutoInstrumentation: viewsAutoInstrumentation, + swiftUIViewInstrumentation: SwiftUIRUMViewsHandler(dateProvider: dateProvider), + userActionsAutoInstrumentation: userActionsAutoInstrumentation, + longTasks: longTasks + ) + } + + init( + viewsAutoInstrumentation: ViewsAutoInstrumentation?, + swiftUIViewInstrumentation: SwiftUIViewHandler, + userActionsAutoInstrumentation: UserActionsAutoInstrumentation?, + longTasks: LongTaskObserver? + ) { self.viewsAutoInstrumentation = viewsAutoInstrumentation self.userActionsAutoInstrumentation = userActionsAutoInstrumentation self.longTasks = longTasks - self.swiftUIViewInstrumentation = SwiftUIRUMViewsHandler(dateProvider: dateProvider) + self.swiftUIViewInstrumentation = swiftUIViewInstrumentation } func enable() { diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 434df3ed68..13752dbbdb 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -693,6 +693,24 @@ class UIKitRUMUserActionsHandlerMock: UIEventHandler { } } +class SwiftUIViewHandlerMock: SwiftUIViewHandler { + var onSubscribe: ((RUMCommandSubscriber) -> Void)? + var notifyOnAppear: ((String, String, String, [AttributeKey: AttributeValue]) -> Void)? + var notifyOnDisappear: ((String) -> Void)? + + func publish(to subscriber: RUMCommandSubscriber) { + onSubscribe?(subscriber) + } + + func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) { + notifyOnAppear?(identity, name, path, attributes) + } + + func onDisappear(identity: String) { + notifyOnDisappear?(identity) + } +} + class SamplingBasedVitalReaderMock: SamplingBasedVitalReader { var vitalData: Double? diff --git a/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift new file mode 100644 index 0000000000..3e2192c7c2 --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift @@ -0,0 +1,94 @@ +/* + * 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 +import SwiftUI + +@testable import Datadog + +class RUMViewModifierTests: XCTestCase { + let swiftUIHandler = SwiftUIViewHandlerMock() + let subscriber = RUMCommandSubscriberMock() + + @available(iOS 13, *) + struct TestView: View { + let name: String + let attributes: [AttributeKey: AttributeValue] + var body: some View { + EmptyView() + .trackRUMView( + name: name, + attributes: attributes + ) + } + } + + override func setUp() { + super.setUp() + + RUMInstrumentation.instance = RUMInstrumentation( + viewsAutoInstrumentation: nil, + swiftUIViewInstrumentation: swiftUIHandler, + userActionsAutoInstrumentation: nil, + longTasks: nil + ) + + RUMInstrumentation.instance?.publish(to: subscriber) + } + + override func tearDown() { + RUMInstrumentation.instance?.deinitialize() + } + + func testGivenASwiftUIView_WhenItAppearsAndDisappears_ItNotifiesTheRUMInstrumentation() throws { + guard #available(iOS 13, *) else { + return + } + + let expectOnAppear = expectation(description: "SwiftUI.View.onAppear") + let expectOnDisappear = expectation(description: "SwiftUI.View.onDisappear") + + // Given + let viewName: String = .mockRandom() + let viewAttributes = mockRandomAttributes() + let animated = Bool.random() + + var host: UIHostingController? = UIHostingController( + rootView: TestView( + name: viewName, + attributes: viewAttributes + ) + ) + + var viewIdentity: String? + + swiftUIHandler.notifyOnAppear = { identity, name, path, attributes in + viewIdentity = identity + XCTAssertTrue(identity.matches(regex: .uuidRegex)) + XCTAssertEqual(name, viewName) + XCTAssertTrue(path.matches(regex: "\(viewName)\\/[0-9]*")) + self.AssertDictionariesEqual(attributes, viewAttributes) + expectOnAppear.fulfill() + } + + swiftUIHandler.notifyOnDisappear = { identity in + XCTAssertEqual(viewIdentity, identity) + expectOnDisappear.fulfill() + } + + // When + // Trigger the `.onAppear` + host?.viewWillAppear(animated) + host?.viewDidAppear(animated) + + // Then + wait(for: [expectOnAppear], timeout: 5) + + // When + host = nil // Trigger the `.onDisappear` + wait(for: [expectOnDisappear], timeout: 5) + } +} From dcec5b072109a479d7a7a7de0f56d3a370ca5a21 Mon Sep 17 00:00:00 2001 From: maxep Date: Wed, 3 Nov 2021 17:19:24 +0100 Subject: [PATCH 5/8] RUMM-1615 Use native view modifier --- Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift b/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift index fc54b76e04..cdc6f2d342 100644 --- a/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift +++ b/Tests/DatadogTests/Datadog/Utils/SwiftUIExtensionsTests.swift @@ -24,8 +24,8 @@ class SwiftUIExtensionsTests: XCTestCase { return } - let view = TestView().trackRUMView(name: "test") - XCTAssertEqual(view.typeDescription, "ModifiedContent") + let view = TestView().cornerRadius(8) + XCTAssertEqual(view.typeDescription, "ModifiedContent>") } func testBundleIsSwiftUI() { From 47b8d48d804d740bf64daf67ddabbb638ae7dd2b Mon Sep 17 00:00:00 2001 From: maxep Date: Wed, 3 Nov 2021 17:31:34 +0100 Subject: [PATCH 6/8] RUMM-1615 Remove `RUMInstrumentation` compliance to `SwiftUIViewHandler` --- .../RUM/Instrumentation/RUMInstrumentation.swift | 10 ---------- .../Views/SwiftUI/SwiftUIViewModifier.swift | 16 +++++++++------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift index cb1b90c0e9..8e6cebfadc 100644 --- a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift +++ b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift @@ -125,13 +125,3 @@ internal final class RUMInstrumentation: RUMCommandPublisher { } #endif } - -extension RUMInstrumentation: SwiftUIViewHandler { - func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) { - swiftUIViewInstrumentation.onAppear(identity: identity, name: name, path: path, attributes: attributes) - } - - func onDisappear(identity: String) { - swiftUIViewInstrumentation.onDisappear(identity: identity) - } -} diff --git a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift index a8a6bbbeca..d91ed1a19d 100644 --- a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift +++ b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift @@ -26,15 +26,17 @@ internal struct RUMViewModifier: SwiftUI.ViewModifier { func body(content: Content) -> some View { content.onAppear { - RUMInstrumentation.instance?.onAppear( - identity: id, - name: name, - path: path, - attributes: attributes - ) + RUMInstrumentation.instance?.swiftUIViewInstrumentation + .onAppear( + identity: id, + name: name, + path: path, + attributes: attributes + ) } .onDisappear { - RUMInstrumentation.instance?.onDisappear(identity: id) + RUMInstrumentation.instance?.swiftUIViewInstrumentation + .onDisappear(identity: id) } } } From 9fbfc166ee37400684275cc11b61a71af5558ba3 Mon Sep 17 00:00:00 2001 From: maxep Date: Wed, 3 Nov 2021 17:56:42 +0100 Subject: [PATCH 7/8] Revert "RUMM-1615 Add `RUMViewModifier` unit tests" This reverts commit ea8193658d29086d06b23d6c40956f0174f68040. --- Datadog/Datadog.xcodeproj/project.pbxproj | 4 - .../Instrumentation/RUMInstrumentation.swift | 18 +--- .../Datadog/Mocks/RUMFeatureMocks.swift | 18 ---- .../Views/SwiftUI/RUMViewModifierTests.swift | 94 ------------------- 4 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 6cea2c91b8..216c4dc51f 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -514,7 +514,6 @@ 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 */; }; - D2EFF3D52732D48800D09F33 /* RUMViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFF3D42732D48800D09F33 /* RUMViewModifierTests.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 */; }; @@ -1173,7 +1172,6 @@ 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 = ""; }; - D2EFF3D42732D48800D09F33 /* RUMViewModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewModifierTests.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 = ""; }; @@ -3297,7 +3295,6 @@ isa = PBXGroup; children = ( D24985A627292FCC00B4F72D /* SwiftUIRUMViewsHandlerTests.swift */, - D2EFF3D42732D48800D09F33 /* RUMViewModifierTests.swift */, ); path = SwiftUI; sourceTree = ""; @@ -4064,7 +4061,6 @@ 61D980BC24E293F600E03345 /* RUMIntegrationsTests.swift in Sources */, 61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */, 61411B1024EC15AC0012EAB2 /* Casting+RUM.swift in Sources */, - D2EFF3D52732D48800D09F33 /* RUMViewModifierTests.swift in Sources */, 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */, 61FF283024BC5E2D000B3D9B /* RUMEventFileOutputTests.swift in Sources */, 9E986C302677B91400D62490 /* VitalRefreshRateReaderTests.swift in Sources */, diff --git a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift index 8e6cebfadc..e461c7d0d5 100644 --- a/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift +++ b/Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift @@ -57,7 +57,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { // MARK: - Initialization - convenience init( + init( configuration: FeaturesConfiguration.RUM.Instrumentation, dateProvider: DateProvider ) { @@ -83,24 +83,10 @@ internal final class RUMInstrumentation: RUMCommandPublisher { longTasks = LongTaskObserver(threshold: threshold, dateProvider: dateProvider) } - self.init( - viewsAutoInstrumentation: viewsAutoInstrumentation, - swiftUIViewInstrumentation: SwiftUIRUMViewsHandler(dateProvider: dateProvider), - userActionsAutoInstrumentation: userActionsAutoInstrumentation, - longTasks: longTasks - ) - } - - init( - viewsAutoInstrumentation: ViewsAutoInstrumentation?, - swiftUIViewInstrumentation: SwiftUIViewHandler, - userActionsAutoInstrumentation: UserActionsAutoInstrumentation?, - longTasks: LongTaskObserver? - ) { self.viewsAutoInstrumentation = viewsAutoInstrumentation self.userActionsAutoInstrumentation = userActionsAutoInstrumentation self.longTasks = longTasks - self.swiftUIViewInstrumentation = swiftUIViewInstrumentation + self.swiftUIViewInstrumentation = SwiftUIRUMViewsHandler(dateProvider: dateProvider) } func enable() { diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 13752dbbdb..434df3ed68 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -693,24 +693,6 @@ class UIKitRUMUserActionsHandlerMock: UIEventHandler { } } -class SwiftUIViewHandlerMock: SwiftUIViewHandler { - var onSubscribe: ((RUMCommandSubscriber) -> Void)? - var notifyOnAppear: ((String, String, String, [AttributeKey: AttributeValue]) -> Void)? - var notifyOnDisappear: ((String) -> Void)? - - func publish(to subscriber: RUMCommandSubscriber) { - onSubscribe?(subscriber) - } - - func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) { - notifyOnAppear?(identity, name, path, attributes) - } - - func onDisappear(identity: String) { - notifyOnDisappear?(identity) - } -} - class SamplingBasedVitalReaderMock: SamplingBasedVitalReader { var vitalData: Double? diff --git a/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift deleted file mode 100644 index 3e2192c7c2..0000000000 --- a/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/RUMViewModifierTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import XCTest -import SwiftUI - -@testable import Datadog - -class RUMViewModifierTests: XCTestCase { - let swiftUIHandler = SwiftUIViewHandlerMock() - let subscriber = RUMCommandSubscriberMock() - - @available(iOS 13, *) - struct TestView: View { - let name: String - let attributes: [AttributeKey: AttributeValue] - var body: some View { - EmptyView() - .trackRUMView( - name: name, - attributes: attributes - ) - } - } - - override func setUp() { - super.setUp() - - RUMInstrumentation.instance = RUMInstrumentation( - viewsAutoInstrumentation: nil, - swiftUIViewInstrumentation: swiftUIHandler, - userActionsAutoInstrumentation: nil, - longTasks: nil - ) - - RUMInstrumentation.instance?.publish(to: subscriber) - } - - override func tearDown() { - RUMInstrumentation.instance?.deinitialize() - } - - func testGivenASwiftUIView_WhenItAppearsAndDisappears_ItNotifiesTheRUMInstrumentation() throws { - guard #available(iOS 13, *) else { - return - } - - let expectOnAppear = expectation(description: "SwiftUI.View.onAppear") - let expectOnDisappear = expectation(description: "SwiftUI.View.onDisappear") - - // Given - let viewName: String = .mockRandom() - let viewAttributes = mockRandomAttributes() - let animated = Bool.random() - - var host: UIHostingController? = UIHostingController( - rootView: TestView( - name: viewName, - attributes: viewAttributes - ) - ) - - var viewIdentity: String? - - swiftUIHandler.notifyOnAppear = { identity, name, path, attributes in - viewIdentity = identity - XCTAssertTrue(identity.matches(regex: .uuidRegex)) - XCTAssertEqual(name, viewName) - XCTAssertTrue(path.matches(regex: "\(viewName)\\/[0-9]*")) - self.AssertDictionariesEqual(attributes, viewAttributes) - expectOnAppear.fulfill() - } - - swiftUIHandler.notifyOnDisappear = { identity in - XCTAssertEqual(viewIdentity, identity) - expectOnDisappear.fulfill() - } - - // When - // Trigger the `.onAppear` - host?.viewWillAppear(animated) - host?.viewDidAppear(animated) - - // Then - wait(for: [expectOnAppear], timeout: 5) - - // When - host = nil // Trigger the `.onDisappear` - wait(for: [expectOnDisappear], timeout: 5) - } -} From b59211ebfbba0d5b4fbfb6801d097ddc38e1d7d0 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 4 Nov 2021 11:02:36 +0100 Subject: [PATCH 8/8] RUMM-1615 Update identity property name and documentation --- .../SwiftUIRootViewController.swift | 13 +-- .../SwiftUI/SwiftUIRUMViewsHandler.swift | 4 +- .../Views/SwiftUI/SwiftUIViewModifier.swift | 6 +- .../RUMMonitor/Scopes/RUMViewIdentity.swift | 4 - .../SwiftUI/SwiftUIRUMViewsHandlerTests.swift | 88 +++++++++---------- 5 files changed, 56 insertions(+), 59 deletions(-) diff --git a/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift index c8627c637e..f213b89eb5 100644 --- a/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift +++ b/Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift @@ -8,21 +8,22 @@ import Foundation import SwiftUI import Datadog -@available(iOS 13, *) /// 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()) } } -@available(iOS 13, *) /// The root view of the SwiftUI instrumentation test. /// -/// This view creates a navigation stack and present a`ScreenView` as fist view. +/// This view creates a `SwiftUI.TabView` to present +/// navigation contexts.. +@available(iOS 13, *) struct RootView: View { var body: some View { TabView { @@ -79,11 +80,11 @@ struct RootView: View { } } -@available(iOS 13, *) /// A basic Screen View at a given index in the stack. /// -/// This view presents a single navigation button to push a -/// `UIScreenView` onto 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. diff --git a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift index 070deb7e81..7bc4cda09d 100644 --- a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift +++ b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandler.swift @@ -100,7 +100,7 @@ internal final class SwiftUIRUMViewsHandler: SwiftUIViewHandler { /// Respond to a `SwiftUI.View.onAppear` event. /// /// - Parameters: - /// - key: The appearing `SwiftUI.View` key. + /// - 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]) { @@ -130,7 +130,7 @@ internal final class SwiftUIRUMViewsHandler: SwiftUIViewHandler { /// Respond to a `SwiftUI.View.onDisappear` event. /// - /// - Parameter key: The disappearing `SwiftUI.View` key. + /// - 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 diff --git a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift index d91ed1a19d..c6906dc601 100644 --- a/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift +++ b/Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewModifier.swift @@ -13,7 +13,7 @@ import SwiftUI internal struct RUMViewModifier: SwiftUI.ViewModifier { /// The Content View identifier. /// The id will be unique per modified view. - let id: String = UUID().uuidString + let identity: String = UUID().uuidString /// View Name used for RUM Explorer. let name: String @@ -28,7 +28,7 @@ internal struct RUMViewModifier: SwiftUI.ViewModifier { content.onAppear { RUMInstrumentation.instance?.swiftUIViewInstrumentation .onAppear( - identity: id, + identity: identity, name: name, path: path, attributes: attributes @@ -36,7 +36,7 @@ internal struct RUMViewModifier: SwiftUI.ViewModifier { } .onDisappear { RUMInstrumentation.instance?.swiftUIViewInstrumentation - .onDisappear(identity: id) + .onDisappear(identity: identity) } } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift index 6c9e2ca6bf..55d240095e 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentity.swift @@ -63,10 +63,6 @@ extension String: RUMViewIdentifiable { var defaultViewPath: String { self } } -internal func == (lhs: RUMViewIdentifiable?, rhs: RUMViewIdentifiable) -> Bool { - return lhs?.equals(rhs) ?? false -} - // MARK: - `RUMViewIdentity` /// Manages the `RUMViewIdentifiable` by using either reference or value semantic. diff --git a/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift index 6b6ca6b355..f7a321ace9 100644 --- a/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIRUMViewsHandlerTests.swift @@ -25,14 +25,14 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { func testWhenOnAppear_itStartsRUMView() throws { // Given - let viewKey: String = UUID().uuidString + let viewIdentity: String = UUID().uuidString let viewName: String = .mockRandom() let viewPath: String = .mockRandom() let viewAttributes = mockRandomAttributes() // When handler.onAppear( - identity: viewKey, + identity: viewIdentity, name: viewName, path: viewPath, attributes: viewAttributes @@ -43,7 +43,7 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { let command = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) XCTAssertEqual(command.time, .mockDecember15th2019At10AMUTC()) - XCTAssertTrue(command.identity.equals(viewKey)) + XCTAssertTrue(command.identity.equals(viewIdentity)) XCTAssertEqual(command.name, viewName) XCTAssertEqual(command.path, viewPath) AssertDictionariesEqual(command.attributes, viewAttributes) @@ -51,26 +51,26 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { func testWhenOnAppear_itStopsPreviousRUMView() throws { // Given - let view1Key: String = UUID().uuidString + let view1Identity: String = UUID().uuidString let view1Name: String = .mockRandom() let view1Path: String = .mockRandom() let view1Attributes = mockRandomAttributes() - let view2Key: String = UUID().uuidString + let view2Identity: String = UUID().uuidString let view2Name: String = .mockRandom() let view2Path: String = .mockRandom() let view2Attributes = mockRandomAttributes() // When handler.onAppear( - identity: view1Key, + identity: view1Identity, name: view1Name, path: view1Path, attributes: view1Attributes ) handler.onAppear( - identity: view2Key, + identity: view2Identity, name: view2Name, path: view2Path, attributes: view2Attributes @@ -83,31 +83,31 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { let stopCommand = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) let startCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) - XCTAssertTrue(startCommand1.identity.equals(view1Key)) + XCTAssertTrue(startCommand1.identity.equals(view1Identity)) AssertDictionariesEqual(startCommand1.attributes, view1Attributes) - XCTAssertTrue(stopCommand.identity.equals(view1Key)) + XCTAssertTrue(stopCommand.identity.equals(view1Identity)) XCTAssertEqual(stopCommand.attributes.count, 0) - XCTAssertTrue(startCommand2.identity.equals(view2Key)) + XCTAssertTrue(startCommand2.identity.equals(view2Identity)) AssertDictionariesEqual(startCommand2.attributes, view2Attributes) } func testWhenOnAppear_itDoesNotStartTheSameRUMViewTwice() throws { // Given - let viewKey: String = UUID().uuidString + let viewIdentity: String = UUID().uuidString let viewName: String = .mockRandom() let viewPath: String = .mockRandom() let viewAttributes = mockRandomAttributes() // When handler.onAppear( - identity: viewKey, + identity: viewIdentity, name: viewName, path: viewPath, attributes: viewAttributes ) handler.onAppear( - identity: viewKey, + identity: viewIdentity, name: viewName, path: viewPath, attributes: viewAttributes @@ -122,10 +122,10 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { func testWhenOnDisappear_itDoesNotSendAnyCommand() { // Given - let viewKey: String = UUID().uuidString + let viewIdentity: String = UUID().uuidString // When - handler.onDisappear(identity: viewKey) + handler.onDisappear(identity: viewIdentity) // Then XCTAssertEqual(commandSubscriber.receivedCommands.count, 0) @@ -133,20 +133,20 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { func testGivenAppearedView_whenOnDisappear_itSopsTheRUMView() throws { // Given - let viewKey: String = UUID().uuidString + let viewIdentity: String = UUID().uuidString let viewName: String = .mockRandom() let viewPath: String = .mockRandom() let viewAttributes = mockRandomAttributes() // When handler.onAppear( - identity: viewKey, + identity: viewIdentity, name: viewName, path: viewPath, attributes: viewAttributes ) - handler.onDisappear(identity: viewKey) + handler.onDisappear(identity: viewIdentity) // Then XCTAssertEqual(commandSubscriber.receivedCommands.count, 2) @@ -154,40 +154,40 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { let startCommand = try XCTUnwrap(commandSubscriber.receivedCommands[0] as? RUMStartViewCommand) let stopCommand = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) - XCTAssertTrue(startCommand.identity.equals(viewKey)) + XCTAssertTrue(startCommand.identity.equals(viewIdentity)) AssertDictionariesEqual(startCommand.attributes, viewAttributes) - XCTAssertTrue(stopCommand.identity.equals(viewKey)) + XCTAssertTrue(stopCommand.identity.equals(viewIdentity)) XCTAssertEqual(stopCommand.attributes.count, 0) } func testGiven2AppearedView_whenTheFirstDisappears_itDoesNotStopItTwice() throws { // Given - let view1Key: String = UUID().uuidString + let view1Identity: String = UUID().uuidString let view1Name: String = .mockRandom() let view1Path: String = .mockRandom() let view1Attributes = mockRandomAttributes() - let view2Key: String = UUID().uuidString + let view2Identity: String = UUID().uuidString let view2Name: String = .mockRandom() let view2Path: String = .mockRandom() let view2Attributes = mockRandomAttributes() // When handler.onAppear( - identity: view1Key, + identity: view1Identity, name: view1Name, path: view1Path, attributes: view1Attributes ) handler.onAppear( - identity: view2Key, + identity: view2Identity, name: view2Name, path: view2Path, attributes: view2Attributes ) - handler.onDisappear(identity: view1Key) + handler.onDisappear(identity: view1Identity) // Then XCTAssertEqual(commandSubscriber.receivedCommands.count, 3) @@ -196,41 +196,41 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { let stopCommand1 = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) let startCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) - XCTAssertTrue(startCommand1.identity.equals(view1Key)) + XCTAssertTrue(startCommand1.identity.equals(view1Identity)) AssertDictionariesEqual(startCommand1.attributes, view1Attributes) - XCTAssertTrue(stopCommand1.identity.equals(view1Key)) - XCTAssertTrue(startCommand2.identity.equals(view2Key)) + XCTAssertTrue(stopCommand1.identity.equals(view1Identity)) + XCTAssertTrue(startCommand2.identity.equals(view2Identity)) AssertDictionariesEqual(startCommand2.attributes, view2Attributes) } func testGiven2AppearedView_whenTheLastDisappears_itRestartsThePreviousRUMView() throws { // Given - let view1Key: String = UUID().uuidString + let view1Identity: String = UUID().uuidString let view1Name: String = .mockRandom() let view1Path: String = .mockRandom() let view1Attributes = mockRandomAttributes() - let view2Key: String = UUID().uuidString + let view2Identity: String = UUID().uuidString let view2Name: String = .mockRandom() let view2Path: String = .mockRandom() let view2Attributes = mockRandomAttributes() // When handler.onAppear( - identity: view1Key, + identity: view1Identity, name: view1Name, path: view1Path, attributes: view1Attributes ) handler.onAppear( - identity: view2Key, + identity: view2Identity, name: view2Name, path: view2Path, attributes: view2Attributes ) - handler.onDisappear(identity: view2Key) + handler.onDisappear(identity: view2Identity) // Then XCTAssertEqual(commandSubscriber.receivedCommands.count, 5) @@ -241,13 +241,13 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { let stopCommand2 = try XCTUnwrap(commandSubscriber.receivedCommands[3] as? RUMStopViewCommand) let startCommand3 = try XCTUnwrap(commandSubscriber.receivedCommands[4] as? RUMStartViewCommand) - XCTAssertTrue(startCommand1.identity.equals(view1Key)) + XCTAssertTrue(startCommand1.identity.equals(view1Identity)) AssertDictionariesEqual(startCommand1.attributes, view1Attributes) - XCTAssertTrue(stopCommand1.identity.equals(view1Key)) - XCTAssertTrue(startCommand2.identity.equals(view2Key)) + XCTAssertTrue(stopCommand1.identity.equals(view1Identity)) + XCTAssertTrue(startCommand2.identity.equals(view2Identity)) AssertDictionariesEqual(startCommand2.attributes, view2Attributes) - XCTAssertTrue(stopCommand2.identity.equals(view2Key)) - XCTAssertTrue(startCommand3.identity.equals(view1Key)) + XCTAssertTrue(stopCommand2.identity.equals(view2Identity)) + XCTAssertTrue(startCommand3.identity.equals(view1Identity)) AssertDictionariesEqual(startCommand1.attributes, view1Attributes) } @@ -255,14 +255,14 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { func testGivenRUMViewStarted_whenAppStateChanges_itStopsAndRestartsRUMView() throws { // Given - let viewKey: String = UUID().uuidString + let viewIdentity: String = UUID().uuidString let viewName: String = .mockRandom() let viewPath: String = .mockRandom() let viewAttributes = mockRandomAttributes() // When handler.onAppear( - identity: viewKey, + identity: viewIdentity, name: viewName, path: viewPath, attributes: viewAttributes @@ -277,10 +277,10 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { let stopCommand = try XCTUnwrap(commandSubscriber.receivedCommands[1] as? RUMStopViewCommand) let startCommand = try XCTUnwrap(commandSubscriber.receivedCommands[2] as? RUMStartViewCommand) - XCTAssertTrue(stopCommand.identity.equals(viewKey)) + XCTAssertTrue(stopCommand.identity.equals(viewIdentity)) XCTAssertEqual(stopCommand.attributes.count, 0) XCTAssertEqual(stopCommand.time, .mockDecember15th2019At10AMUTC()) - XCTAssertTrue(startCommand.identity.equals(viewKey)) + XCTAssertTrue(startCommand.identity.equals(viewIdentity)) XCTAssertEqual(startCommand.path, viewPath) XCTAssertEqual(startCommand.name, viewName) AssertDictionariesEqual(startCommand.attributes, viewAttributes) @@ -289,10 +289,10 @@ class SwiftUIRUMViewsHandlerTests: XCTestCase { func testGivenRUMViewDidNotStart_whenAppStateChanges_itDoesNothing() throws { // Given - let viewKey: String = UUID().uuidString + let viewIdentity: String = UUID().uuidString // When - handler.onDisappear(identity: viewKey) + handler.onDisappear(identity: viewIdentity) notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) dateProvider.advance(bySeconds: 1)