diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index e88724313f..ff9d3637cb 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -500,6 +500,7 @@ 9E307C3224C8846D0039607E /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E26E6B824C87693000B3270 /* RUMDataModels.swift */; }; 9E359F4E26CD518D001E25E9 /* LongTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */; }; 9E36D92224373EA700BFBDB7 /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; + 9E53889C2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E53889B2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift */; }; 9E544A4F24753C6E00E83072 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */; }; 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */; }; 9E55407C25812D1C00F6E3AD /* RUMMonitor+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E55407B25812D1C00F6E3AD /* RUMMonitor+objc.swift */; }; @@ -515,6 +516,19 @@ 9E989A4225F640D100235FC3 /* AppStateListenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E989A4125F640D100235FC3 /* AppStateListenerTests.swift */; }; 9E9973F1268DF69500D8059B /* VitalInfoSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */; }; 9EA3CA6926775A3500B16871 /* VitalRefreshRateReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */; }; + 9EA8A7F1275E1518007D6FDB /* HostsSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA8A7F0275E1518007D6FDB /* HostsSanitizerTests.swift */; }; + 9EA8A7F82768A72B007D6FDB /* WebLogEventConsumerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA8A7F72768A72B007D6FDB /* WebLogEventConsumerTests.swift */; }; + 9EA95C1C2791C9BE00F6C1F3 /* WebViewTrackingFixtureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA95C192791C9BE00F6C1F3 /* WebViewTrackingFixtureViewController.swift */; }; + 9EA95C1D2791C9BE00F6C1F3 /* WebViewTrackingScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9EA95C1A2791C9BE00F6C1F3 /* WebViewTrackingScenario.storyboard */; }; + 9EA95C1E2791C9BE00F6C1F3 /* WebViewScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA95C1B2791C9BE00F6C1F3 /* WebViewScenarios.swift */; }; + 9EA95C212791C9E200F6C1F3 /* WebViewScenarioTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA95C202791C9E200F6C1F3 /* WebViewScenarioTest.swift */; }; + 9EAF0CF6275A21100044E8CA /* WKUserContentController+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAF0CF5275A21100044E8CA /* WKUserContentController+DatadogTests.swift */; }; + 9EAF0CF8275A2FDC0044E8CA /* HostsSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAF0CF7275A2FDC0044E8CA /* HostsSanitizer.swift */; }; + 9EB4B862274E79D50041CD03 /* WKUserContentController+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB4B861274E79D50041CD03 /* WKUserContentController+Datadog.swift */; }; + 9EB4B864274FAB410041CD03 /* WebLogEventConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB4B863274FAB410041CD03 /* WebLogEventConsumer.swift */; }; + 9EB4B866274FAB4C0041CD03 /* WebRUMEventConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB4B865274FAB4C0041CD03 /* WebRUMEventConsumer.swift */; }; + 9EB4B868275103E40041CD03 /* WebEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB4B867275103E40041CD03 /* WebEventBridge.swift */; }; + 9EB4B86C27510AF90041CD03 /* WebEventBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB4B86B27510AF90041CD03 /* WebEventBridgeTests.swift */; }; 9EC2835A26CFEE0B00FACF1C /* RUMMobileVitalsScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2835926CFEE0B00FACF1C /* RUMMobileVitalsScenarioTests.swift */; }; 9EC2835E26CFF57A00FACF1C /* RUMMobileVitalsScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9EC2835D26CFF57A00FACF1C /* RUMMobileVitalsScenario.storyboard */; }; 9EC2836026CFF59400FACF1C /* RUMMobileVitalsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2835F26CFF59400FACF1C /* RUMMobileVitalsViewController.swift */; }; @@ -1184,6 +1198,7 @@ 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoSamplerTests.swift; sourceTree = ""; }; 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongTaskObserver.swift; sourceTree = ""; }; 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensionsTests.swift; sourceTree = ""; }; + 9E53889B2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRUMEventConsumerTests.swift; sourceTree = ""; }; 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzler.swift; sourceTree = ""; }; 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzlerTests.swift; sourceTree = ""; }; 9E55407B25812D1C00F6E3AD /* RUMMonitor+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMMonitor+objc.swift"; sourceTree = ""; }; @@ -1200,6 +1215,19 @@ 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoSampler.swift; sourceTree = ""; }; 9E9EB37624468CE90002C80B /* Datadog.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Datadog.modulemap; sourceTree = ""; }; 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalRefreshRateReader.swift; sourceTree = ""; }; + 9EA8A7F0275E1518007D6FDB /* HostsSanitizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsSanitizerTests.swift; sourceTree = ""; }; + 9EA8A7F72768A72B007D6FDB /* WebLogEventConsumerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLogEventConsumerTests.swift; sourceTree = ""; }; + 9EA95C192791C9BE00F6C1F3 /* WebViewTrackingFixtureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewTrackingFixtureViewController.swift; sourceTree = ""; }; + 9EA95C1A2791C9BE00F6C1F3 /* WebViewTrackingScenario.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = WebViewTrackingScenario.storyboard; sourceTree = ""; }; + 9EA95C1B2791C9BE00F6C1F3 /* WebViewScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewScenarios.swift; sourceTree = ""; }; + 9EA95C202791C9E200F6C1F3 /* WebViewScenarioTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewScenarioTest.swift; sourceTree = ""; }; + 9EAF0CF5275A21100044E8CA /* WKUserContentController+DatadogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+DatadogTests.swift"; sourceTree = ""; }; + 9EAF0CF7275A2FDC0044E8CA /* HostsSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsSanitizer.swift; sourceTree = ""; }; + 9EB4B861274E79D50041CD03 /* WKUserContentController+Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+Datadog.swift"; sourceTree = ""; }; + 9EB4B863274FAB410041CD03 /* WebLogEventConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLogEventConsumer.swift; sourceTree = ""; }; + 9EB4B865274FAB4C0041CD03 /* WebRUMEventConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRUMEventConsumer.swift; sourceTree = ""; }; + 9EB4B867275103E40041CD03 /* WebEventBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEventBridge.swift; sourceTree = ""; }; + 9EB4B86B27510AF90041CD03 /* WebEventBridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEventBridgeTests.swift; sourceTree = ""; }; 9EC2835926CFEE0B00FACF1C /* RUMMobileVitalsScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMobileVitalsScenarioTests.swift; sourceTree = ""; }; 9EC2835D26CFF57A00FACF1C /* RUMMobileVitalsScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMMobileVitalsScenario.storyboard; sourceTree = ""; }; 9EC2835F26CFF59400FACF1C /* RUMMobileVitalsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMobileVitalsViewController.swift; sourceTree = ""; }; @@ -1501,6 +1529,7 @@ 61363D9C24D999F70084CD6F /* DDError.swift */, 6139CD702589FAFD007E8BB7 /* Retrying.swift */, 611529A425E3DD51004F740E /* ValuePublisher.swift */, + 9EAF0CF7275A2FDC0044E8CA /* HostsSanitizer.swift */, 613C6B8F2768FDDE00870CBF /* Sampler.swift */, ); path = Utils; @@ -1951,6 +1980,7 @@ 6156CB9B24E18224008CB2B2 /* TracingWithRUMIntegration.swift */, 61FC5F4425CC23C9006BB4DE /* RUMWithCrashContextIntegration.swift */, E13A880B257922EC004FB174 /* EnvironmentSpanIntegration.swift */, + 9EB4B860274E79620041CD03 /* WebView */, 6112B11C25C84E7F00B37771 /* CrashReporting */, ); path = FeaturesIntegration; @@ -2033,6 +2063,7 @@ 6111542325C992D9007C84C9 /* CrashReporting */, 611EA12B2580F40E00BC0E56 /* TrackingConsent */, 6164AE7D252B4CE2000D78C4 /* URLSession */, + 9EA95C182791C9BE00F6C1F3 /* WebView */, ); path = Scenarios; sourceTree = ""; @@ -2599,6 +2630,7 @@ 618C365E248E85B400520CDE /* DateFormattingTests.swift */, 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */, 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */, + 9EA8A7F0275E1518007D6FDB /* HostsSanitizerTests.swift */, 613C6B912768FF3100870CBF /* SamplerTests.swift */, ); path = Utils; @@ -3046,6 +3078,7 @@ 61E5332D24B75DC7003D6C4E /* RUM */ = { isa = PBXGroup; children = ( + 9EB4B86A27510AC80041CD03 /* WebView */, B3FC3C1226526F4100DEED9E /* RUMVitals */, 61E5332E24B75DE2003D6C4E /* RUMFeatureTests.swift */, 618715FA24DC5EE700FC0F69 /* DataModels */, @@ -3173,6 +3206,7 @@ 61F3CD9F2511070300C816E5 /* RUM */, 61B6811D25F0E8480015B4AF /* CrashReporting */, 611EA15325815EDC00BC0E56 /* TrackingConsent */, + 9EA95C1F2791C9E200F6C1F3 /* WebView */, ); path = Scenarios; sourceTree = ""; @@ -3365,6 +3399,46 @@ path = ../Sources/_Datadog_Private; sourceTree = ""; }; + 9EA95C182791C9BE00F6C1F3 /* WebView */ = { + isa = PBXGroup; + children = ( + 9EA95C192791C9BE00F6C1F3 /* WebViewTrackingFixtureViewController.swift */, + 9EA95C1A2791C9BE00F6C1F3 /* WebViewTrackingScenario.storyboard */, + 9EA95C1B2791C9BE00F6C1F3 /* WebViewScenarios.swift */, + ); + path = WebView; + sourceTree = ""; + }; + 9EA95C1F2791C9E200F6C1F3 /* WebView */ = { + isa = PBXGroup; + children = ( + 9EA95C202791C9E200F6C1F3 /* WebViewScenarioTest.swift */, + ); + path = WebView; + sourceTree = ""; + }; + 9EB4B860274E79620041CD03 /* WebView */ = { + isa = PBXGroup; + children = ( + 9EB4B861274E79D50041CD03 /* WKUserContentController+Datadog.swift */, + 9EB4B867275103E40041CD03 /* WebEventBridge.swift */, + 9EB4B863274FAB410041CD03 /* WebLogEventConsumer.swift */, + 9EB4B865274FAB4C0041CD03 /* WebRUMEventConsumer.swift */, + ); + path = WebView; + sourceTree = ""; + }; + 9EB4B86A27510AC80041CD03 /* WebView */ = { + isa = PBXGroup; + children = ( + 9EB4B86B27510AF90041CD03 /* WebEventBridgeTests.swift */, + 9EAF0CF5275A21100044E8CA /* WKUserContentController+DatadogTests.swift */, + 9EA8A7F72768A72B007D6FDB /* WebLogEventConsumerTests.swift */, + 9E53889B2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift */, + ); + path = WebView; + sourceTree = ""; + }; 9EC2835C26CFF56B00FACF1C /* MobileVitals */ = { isa = PBXGroup; children = ( @@ -3853,6 +3927,7 @@ D2F5BB36271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard in Resources */, 611EA13C2580F77400BC0E56 /* TrackingConsentScenario.storyboard in Resources */, 61441C1124616DEC003D8BB8 /* LaunchScreen.storyboard in Resources */, + 9EA95C1D2791C9BE00F6C1F3 /* WebViewTrackingScenario.storyboard in Resources */, 61337039250F852E00236D58 /* RUMManualInstrumentationScenario.storyboard in Resources */, 6193DCA4251B5691009B8011 /* RUMTapActionScenario.storyboard in Resources */, 6167ACC7251A0BCE0012B4D0 /* NSURLSessionScenario.storyboard in Resources */, @@ -4058,10 +4133,12 @@ 61E909F124A24DD3005EA2DE /* OTReference.swift in Sources */, 6116563B25D2A6C90070EC03 /* ArbitraryDataWriter.swift in Sources */, 61216276247D1CD700AC5D67 /* LoggingForTracingAdapter.swift in Sources */, + 9EB4B866274FAB4C0041CD03 /* WebRUMEventConsumer.swift in Sources */, 61E909EF24A24DD3005EA2DE /* Global.swift in Sources */, D2EFF3D32731822A00D09F33 /* RUMViewsHandler.swift in Sources */, 61133BDD2423979B00786299 /* InternalLoggers.swift in Sources */, 6105D1A02508F1600040DD22 /* LoggingWithActiveSpanIntegration.swift in Sources */, + 9EB4B864274FAB410041CD03 /* WebLogEventConsumer.swift in Sources */, 61EF78B1257E2E7A00EDCCB3 /* MoveDataMigrator.swift in Sources */, 61133BDC2423979B00786299 /* Logger.swift in Sources */, 6114FE1625766B310084E372 /* TrackingConsent.swift in Sources */, @@ -4147,11 +4224,13 @@ 613E793B2577B6EE00DFCC17 /* DataReader.swift in Sources */, 614B0A5324EBFE5500A2A780 /* DDRUMMonitor.swift in Sources */, 61133BE32423979B00786299 /* UserInfoProvider.swift in Sources */, + 9EB4B868275103E40041CD03 /* WebEventBridge.swift in Sources */, 61133BE02423979B00786299 /* Datadog.swift in Sources */, 61133BCB2423979B00786299 /* CarrierInfoProvider.swift in Sources */, 61C5A89024509AA700DA608C /* TracingFeature.swift in Sources */, 61E5333624B84B43003D6C4E /* RUMMonitor.swift in Sources */, 6156CB9824DEFD44008CB2B2 /* LoggingWithRUMIntegration.swift in Sources */, + 9EB4B862274E79D50041CD03 /* WKUserContentController+Datadog.swift in Sources */, 61133BD62423979B00786299 /* DataUploader.swift in Sources */, 619E16F12578E89700B2516B /* DeleteAllDataMigrator.swift in Sources */, 61494CBA24CB126F0082C633 /* RUMUserActionScope.swift in Sources */, @@ -4221,6 +4300,7 @@ 6114FE0F257667D40084E372 /* ConsentAwareDataWriter.swift in Sources */, 61C5A88624509A0C00DA608C /* TracingUUIDGenerator.swift in Sources */, 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */, + 9EAF0CF8275A2FDC0044E8CA /* HostsSanitizer.swift in Sources */, 61C5A88C24509A0C00DA608C /* HTTPHeadersWriter.swift in Sources */, 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */, 614B0A4B24EBC43D00A2A780 /* RUMUserInfoProvider.swift in Sources */, @@ -4266,6 +4346,7 @@ 61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */, 61B038BA2527257B00518F3C /* URLSessionAutoInstrumentationMocks.swift in Sources */, 61133C672423990D00786299 /* LogConsoleOutputTests.swift in Sources */, + 9E53889C2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift in Sources */, 6114FDEC257659E90084E372 /* FeatureDirectoriesMock.swift in Sources */, 61122EE825B1C92500F9C7F5 /* SpanSanitizerTests.swift in Sources */, 61B5E42526DFAFBC000B0A5F /* DDGlobal+apiTests.m in Sources */, @@ -4275,6 +4356,7 @@ 61F9CABA2513A7F5000A5E61 /* RUMSessionMatcher.swift in Sources */, 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, 61AD4E182451C7FF006E34EA /* TracingFeatureMocks.swift in Sources */, + 9EAF0CF6275A21100044E8CA /* WKUserContentController+DatadogTests.swift in Sources */, 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */, 615A4A8724A3452800233986 /* DDTracerConfigurationTests.swift in Sources */, 613E81F725A743600084B751 /* RUMEventsMapperTests.swift in Sources */, @@ -4306,6 +4388,7 @@ 61BBD19724ED50040023E65F /* FeaturesConfigurationTests.swift in Sources */, 61133C612423990D00786299 /* HTTPClientTests.swift in Sources */, 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */, + 9EB4B86C27510AF90041CD03 /* WebEventBridgeTests.swift in Sources */, 61B0387C252724AB00518F3C /* DDURLSessionDelegateTests.swift in Sources */, 61AADBDD263C7ECF008ABC6F /* EquatableInTests.swift in Sources */, 61133C5E2423990D00786299 /* DataUploadDelayTests.swift in Sources */, @@ -4368,10 +4451,12 @@ 61B5E42B26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m in Sources */, 61F8CC092469295500FE2908 /* DatadogConfigurationBuilderTests.swift in Sources */, 61F1A623249B811200075390 /* Encoding.swift in Sources */, + 9EA8A7F82768A72B007D6FDB /* WebLogEventConsumerTests.swift in Sources */, 6114FE3B25768AA90084E372 /* ConsentProviderTests.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, 61815C06278867D1004A666C /* KronosMonitorTests.swift in Sources */, 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */, + 9EA8A7F1275E1518007D6FDB /* HostsSanitizerTests.swift in Sources */, D244B3A3271EDACD003E1B29 /* SwiftUIExtensionsTests.swift in Sources */, 61F187FC25FA7DD60022CE9A /* InternalMonitoringFeatureTests.swift in Sources */, 61B5E42126DF85C7000B0A5F /* DDRUMMonitor+apiTests.m in Sources */, @@ -4436,6 +4521,7 @@ D2F5BB382718331800BDE2A4 /* SwiftUIRootViewController.swift in Sources */, 618DCFE124C766F500589570 /* SendRUMFixture2ViewController.swift in Sources */, 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */, + 9EA95C1E2791C9BE00F6C1F3 /* WebViewScenarios.swift in Sources */, 61F9CA8025125C01000A5E61 /* RUMNCSScreen3ViewController.swift in Sources */, 6164AE89252B4ECA000D78C4 /* SendThirdPartyRequestsViewController.swift in Sources */, 611EA14225810E1900BC0E56 /* TSHomeViewController.swift in Sources */, @@ -4472,6 +4558,7 @@ 617247AF25DA9BEA007085B3 /* CrashReportingObjcHelpers.m in Sources */, 6193DCE1251B692C009B8011 /* RUMTASTableViewController.swift in Sources */, 6111544825C9A88B007C84C9 /* PersistenceHelper.swift in Sources */, + 9EA95C1C2791C9BE00F6C1F3 /* WebViewTrackingFixtureViewController.swift in Sources */, 61D50C462580EF19006038A3 /* TracingScenarios.swift in Sources */, 61776CED273BEA5500F93802 /* DebugRUMSessionViewController.swift in Sources */, 618DCFE324C766FB00589570 /* SendRUMFixture3ViewController.swift in Sources */, @@ -4501,6 +4588,7 @@ 61441C4124617013003D8BB8 /* LoggingScenarioTests.swift in Sources */, 612D8F8125AF1C74000E2E09 /* RUMScrubbingScenarioTests.swift in Sources */, 613B77382521E80800155458 /* RUMTabBarControllerScenarioTests.swift in Sources */, + 9EA95C212791C9E200F6C1F3 /* WebViewScenarioTest.swift in Sources */, 61441C4B24618052003D8BB8 /* SpanMatcher.swift in Sources */, 6167ACFD251A22E00012B4D0 /* TracingURLSessionScenarioTests.swift in Sources */, 611EA16625825FB300BC0E56 /* TrackingConsentScenarioTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme index 4d7540a66d..dff85c19f2 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme @@ -111,6 +111,16 @@ value = "$(BITRISE_BUILD_URL)" isEnabled = "YES"> + + + + + + + + + + + + + + + + + + RUMView? { + if viewController is ShopistWebviewViewController { + return nil // do not consider the webview itself as RUM view + } else { + return defaultPredicate.rumView(for: viewController) + } + } +} + +final class WebViewTrackingScenario: TestScenario { + static var storyboardName: String = "WebViewTrackingScenario" + + func configureSDK(builder: Datadog.Configuration.Builder) { + _ = builder + .trackUIKitRUMViews(using: WebViewTrackingScenarioPredicate()) + .enableLogging(true) + .enableRUM(true) + } +} diff --git a/Datadog/Example/Scenarios/WebView/WebViewTrackingFixtureViewController.swift b/Datadog/Example/Scenarios/WebView/WebViewTrackingFixtureViewController.swift new file mode 100644 index 0000000000..2e5de7355d --- /dev/null +++ b/Datadog/Example/Scenarios/WebView/WebViewTrackingFixtureViewController.swift @@ -0,0 +1,43 @@ +/* + * 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 UIKit +import WebKit +import Datadog + +class WebViewTrackingFixtureViewController: UIViewController, WKNavigationDelegate { + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // An action sent from native iOS SDK. + Global.rum.addUserAction(type: .custom, name: "Native action") + + // Opens a webview configured to pass all its Browser SDK events to native iOS SDK. + show(ShopistWebviewViewController(), sender: nil) + } +} + +class ShopistWebviewViewController: UIViewController { + private let request = URLRequest(url: URL(string: "https://shopist.io")!) + private var webView: WKWebView! + + override func viewDidLoad() { + super.viewDidLoad() + + let controller = WKUserContentController() + controller.trackDatadogEvents(in: ["shopist.io"]) + let config = WKWebViewConfiguration() + config.userContentController = controller + + webView = WKWebView(frame: UIScreen.main.bounds, configuration: config) + view.addSubview(webView) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + webView.load(request) + } +} diff --git a/Datadog/Example/Scenarios/WebView/WebViewTrackingScenario.storyboard b/Datadog/Example/Scenarios/WebView/WebViewTrackingScenario.storyboard new file mode 100644 index 0000000000..29f3512548 --- /dev/null +++ b/Datadog/Example/Scenarios/WebView/WebViewTrackingScenario.storyboard @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogCrashReportingIntegrationTests.xctestplan b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogCrashReportingIntegrationTests.xctestplan index 431e392aa1..c070030cdf 100644 --- a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogCrashReportingIntegrationTests.xctestplan +++ b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogCrashReportingIntegrationTests.xctestplan @@ -33,21 +33,9 @@ }, "testTargets" : [ { - "skippedTests" : [ - "IntegrationTests", - "LoggingScenarioTests", - "RUMManualInstrumentationScenarioTests", - "RUMMobileVitalsScenarioTests", - "RUMModalViewsScenarioTests", - "RUMNavigationControllerScenarioTests", - "RUMResourcesScenarioTests", - "RUMScrubbingScenarioTests", - "RUMSwiftUIScenarioTests", - "RUMTabBarControllerScenarioTests", - "RUMTapActionScenarioTests", - "TracingManualInstrumentationScenarioTests", - "TracingURLSessionScenarioTests", - "TrackingConsentScenarioTests" + "selectedTests" : [ + "CrashReportingWithLoggingScenarioTests\/testCrashReportingCollectOrSendWithLoggingScenario()", + "CrashReportingWithRUMScenarioTests\/testCrashReportingCollectOrSendWithRUMScenario()" ], "target" : { "containerPath" : "container:Datadog.xcodeproj", diff --git a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xctestplan b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xctestplan index f25214e204..f302680d7e 100644 --- a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xctestplan +++ b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xctestplan @@ -4,6 +4,68 @@ "id" : "D87CA41D-8EBB-4809-AC70-E3B8317FAAC7", "name" : "TSAN", "options" : { + "environmentVariableEntries" : [ + { + "key" : "DD_TEST_RUNNER", + "value" : "$(DD_TEST_RUNNER)" + }, + { + "key" : "DATADOG_CLIENT_TOKEN", + "value" : "$(DD_SDK_SWIFT_TESTING_CLIENT_TOKEN)" + }, + { + "key" : "DD_ENV", + "value" : "$(DD_SDK_SWIFT_TESTING_ENV)" + }, + { + "key" : "DD_SERVICE", + "value" : "$(DD_SDK_SWIFT_TESTING_SERVICE)" + }, + { + "key" : "DD_DISABLE_SDKIOS_INTEGRATION", + "value" : "1" + }, + { + "key" : "DD_DISABLE_HEADERS_INJECTION", + "value" : "1" + }, + { + "key" : "DD_ENABLE_RECORD_PAYLOAD", + "value" : "1" + }, + { + "key" : "SRCROOT", + "value" : "$(SRCROOT)" + }, + { + "key" : "BITRISE_SOURCE_DIR", + "value" : "$(BITRISE_SOURCE_DIR)" + }, + { + "key" : "BITRISE_TRIGGERED_WORKFLOW_ID", + "value" : "$(BITRISE_TRIGGERED_WORKFLOW_ID)" + }, + { + "key" : "BITRISE_BUILD_SLUG", + "value" : "$(BITRISE_BUILD_SLUG)" + }, + { + "key" : "BITRISE_BUILD_NUMBER", + "value" : "$(BITRISE_BUILD_NUMBER)" + }, + { + "key" : "BITRISE_BUILD_URL", + "value" : "$(BITRISE_BUILD_URL)" + }, + { + "key" : "DD_ENABLE_STDOUT_INSTRUMENTATION", + "value" : "1" + }, + { + "key" : "DD_ENABLE_STDERR_INSTRUMENTATION", + "value" : "1" + } + ], "threadSanitizerEnabled" : true } } diff --git a/Makefile b/Makefile index d970ffe696..df3204a32a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: dependencies xcodeproj-httpservermock templates # The release version of `dd-sdk-swift-testing` to use for tests instrumentation. -DD_SDK_SWIFT_TESTING_VERSION = 1.0.3-beta.1 +DD_SDK_SWIFT_TESTING_VERSION = 1.1.0 define DD_SDK_TESTING_XCCONFIG_CI FRAMEWORK_SEARCH_PATHS=$$(inherited) $$(SRCROOT)/../instrumented-tests/DatadogSDKTesting.xcframework/ios-arm64_x86_64-simulator/\n @@ -17,8 +17,9 @@ export DD_SDK_TESTING_XCCONFIG_CI define DD_SDK_BASE_XCCONFIG // Active compilation conditions - only enabled on local machine:\n // - DD_SDK_ENABLE_INTERNAL_MONITORING - enables Internal Monitoring APIs\n +// - DD_SDK_ENABLE_EXPERIMENTAL_APIS - enables APIs which are not available in released version of the SDK\n // - DD_SDK_COMPILED_FOR_TESTING - conditions the SDK code compiled for testing\n -SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DD_SDK_ENABLE_INTERNAL_MONITORING DD_SDK_COMPILED_FOR_TESTING\n +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DD_SDK_ENABLE_INTERNAL_MONITORING DD_SDK_ENABLE_EXPERIMENTAL_APIS DD_SDK_COMPILED_FOR_TESTING\n \n // To build only active architecture for all configurations. This gives us ~10% build time gain\n // in targets which do not use 'Debug' configuration.\n diff --git a/Sources/Datadog/Core/FeaturesConfiguration.swift b/Sources/Datadog/Core/FeaturesConfiguration.swift index af48d53a45..38aac4540d 100644 --- a/Sources/Datadog/Core/FeaturesConfiguration.swift +++ b/Sources/Datadog/Core/FeaturesConfiguration.swift @@ -115,7 +115,7 @@ extension FeaturesConfiguration { /// /// Throws an error on invalid user input, i.e. broken custom URL. /// Prints a warning if configuration is inconsistent, i.e. RUM is enabled, but RUM Application ID was not specified. - init(configuration: Datadog.Configuration, appContext: AppContext) throws { + init(configuration: Datadog.Configuration, appContext: AppContext, hostsSanitizer: HostsSanitizing = HostsSanitizer()) throws { var logging: Logging? var tracing: Tracing? var rum: RUM? @@ -231,7 +231,10 @@ extension FeaturesConfiguration { if let firstPartyHosts = configuration.firstPartyHosts { if configuration.tracingEnabled || configuration.rumEnabled { urlSessionAutoInstrumentation = URLSessionAutoInstrumentation( - userDefinedFirstPartyHosts: sanitized(firstPartyHosts: firstPartyHosts), + userDefinedFirstPartyHosts: hostsSanitizer.sanitized( + hosts: firstPartyHosts, + warningMessage: "The first party host configured for Datadog SDK is not valid" + ), sdkInternalURLs: [ logsEndpoint.url, tracesEndpoint.url, @@ -318,47 +321,3 @@ private func ifValid(clientToken: String) throws -> String { } return clientToken } - -private func sanitized(firstPartyHosts: Set) -> Set { - let urlRegex = #"^(http|https)://(.*)"# - let hostRegex = #"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])$"# - let ipRegex = #"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"# - - var warnings: [String] = [] - - let array: [String] = firstPartyHosts.compactMap { host in - if host.range(of: urlRegex, options: .regularExpression) != nil { - // if an URL is given instead of the host, take its `host` part - if let sanitizedHost = URL(string: host)?.host { - warnings.append("'\(host)' is an url and will be sanitized to: '\(sanitizedHost)'.") - return sanitizedHost - } else { - warnings.append("'\(host)' is not a valid host name and will be dropped.") - return nil - } - } else if host.range(of: hostRegex, options: .regularExpression) != nil { - // if a valid host name is given, accept it - return host - } else if host.range(of: ipRegex, options: .regularExpression) != nil { - // if a valid IP address is given, accept it - return host - } else if host == "localhost" { - // if "localhost" given, accept it - return host - } else { - // otherwise, drop - warnings.append("'\(host)' is not a valid host name and will be dropped.") - return nil - } - } - - warnings.forEach { warning in - consolePrint( - """ - ⚠️ The first party host configured for Datadog SDK is not valid: \(warning) - """ - ) - } - - return Set(array) -} diff --git a/Sources/Datadog/Core/Utils/HostsSanitizer.swift b/Sources/Datadog/Core/Utils/HostsSanitizer.swift new file mode 100644 index 0000000000..6954d613ae --- /dev/null +++ b/Sources/Datadog/Core/Utils/HostsSanitizer.swift @@ -0,0 +1,57 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal protocol HostsSanitizing { + func sanitized(hosts: Set, warningMessage: String) -> Set +} + +internal struct HostsSanitizer: HostsSanitizing { + func sanitized(hosts: Set, warningMessage: String) -> Set { + let urlRegex = #"^(http|https)://(.*)"# + let hostRegex = #"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])$"# + let ipRegex = #"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"# + + var warnings: [String] = [] + + let array: [String] = hosts.compactMap { host in + if host.range(of: urlRegex, options: .regularExpression) != nil { + // if an URL is given instead of the host, take its `host` part + if let sanitizedHost = URL(string: host)?.host { + warnings.append("'\(host)' is an url and will be sanitized to: '\(sanitizedHost)'.") + return sanitizedHost + } else { + warnings.append("'\(host)' is not a valid host name and will be dropped.") + return nil + } + } else if host.range(of: hostRegex, options: .regularExpression) != nil { + // if a valid host name is given, accept it + return host + } else if host.range(of: ipRegex, options: .regularExpression) != nil { + // if a valid IP address is given, accept it + return host + } else if host == "localhost" { + // if "localhost" given, accept it + return host + } else { + // otherwise, drop + warnings.append("'\(host)' is not a valid host name and will be dropped.") + return nil + } + } + + warnings.forEach { warning in + consolePrint( + """ + ⚠️ \(warningMessage): \(warning) + """ + ) + } + + return Set(array) + } +} diff --git a/Sources/Datadog/CrashReporting/CrashReporter.swift b/Sources/Datadog/CrashReporting/CrashReporter.swift index 1a5d90ade3..2aa97200ed 100644 --- a/Sources/Datadog/CrashReporting/CrashReporter.swift +++ b/Sources/Datadog/CrashReporting/CrashReporter.swift @@ -89,9 +89,11 @@ internal class CrashReporter { queue.async { self.plugin.readPendingCrashReport { [weak self] crashReport in guard let self = self, let availableCrashReport = crashReport else { + userLogger.debug("No pending crash available") return false } + userLogger.debug("Loaded pending crash report") #if DD_SDK_ENABLE_INTERNAL_MONITORING InternalMonitoringFeature.instance?.monitor.sdkLogger .debug("Loaded pending crash report", attributes: availableCrashReport.diagnosticInfo) @@ -166,4 +168,11 @@ internal class CrashReporter { return nil } } + +#if DD_SDK_COMPILED_FOR_TESTING + func deinitialize() { + // Await asynchronous operations completion to safely sink all pending tasks. + queue.sync {} + } +#endif } diff --git a/Sources/Datadog/Datadog.swift b/Sources/Datadog/Datadog.swift index f58ac1b424..6174469eb1 100644 --- a/Sources/Datadog/Datadog.swift +++ b/Sources/Datadog/Datadog.swift @@ -347,9 +347,6 @@ public class Datadog { public static func flushAndDeinitialize() { assert(Datadog.instance != nil, "SDK must be first initialized.") - // Reset internal loggers: - userLogger = createNoOpSDKUserLogger() - // Tear down and deinitialize all features: LoggingFeature.instance?.deinitialize() TracingFeature.instance?.deinitialize() @@ -364,10 +361,14 @@ public class Datadog { // Reset Globals: Global.sharedTracer = DDNoopGlobals.tracer Global.rum = DDNoopRUMMonitor() + Global.crashReporter?.deinitialize() Global.crashReporter = nil // Deinitialize `Datadog`: Datadog.instance = nil + + // Reset internal loggers: + userLogger = createNoOpSDKUserLogger() } #endif } diff --git a/Sources/Datadog/FeaturesIntegration/CrashReporting/CrashReportingWithRUMIntegration.swift b/Sources/Datadog/FeaturesIntegration/CrashReporting/CrashReportingWithRUMIntegration.swift index 3720fb2b86..9c19f468a5 100644 --- a/Sources/Datadog/FeaturesIntegration/CrashReporting/CrashReportingWithRUMIntegration.swift +++ b/Sources/Datadog/FeaturesIntegration/CrashReporting/CrashReportingWithRUMIntegration.swift @@ -101,6 +101,7 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { } else { // We know it is too late for sending RUM view to previous RUM session as it is now stale on backend. // To avoid inconsistency, we only send the RUM error. + userLogger.debug("Sending crash as RUM error.") let rumError = createRUMError(from: crashReport, and: lastRUMViewEvent, crashDate: crashTimings.realCrashDate) rumEventOutput.write(rumEvent: rumError) } @@ -146,7 +147,7 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { crashContext: crashContext ) case .doNotHandle: - // This can mean that the crash happened in background and BET is disabled OR that the previous session was sampled. + userLogger.debug("There was a crash in background, but it is ignored due to Background Event Tracking disabled or sampling.") newRUMView = nil } @@ -191,7 +192,8 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { crashContext: crashContext ) case .doNotHandle: - newRUMView = nil // could mean that the crash happened in background and BET is disabled + userLogger.debug("There was a crash in background, but it is ignored due to Background Event Tracking disabled.") + newRUMView = nil } if let newRUMView = newRUMView { @@ -201,6 +203,7 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { /// Sends given `CrashReport` by linking it to given `rumView` and updating view counts accordingly. private func send(crashReport: DDCrashReport, to rumView: RUMEvent, using realCrashDate: Date) { + userLogger.debug("Updating RUM view with crash report.") let updatedRUMView = updateRUMViewWithNewError(rumView, crashDate: realCrashDate) let rumError = createRUMError(from: crashReport, and: updatedRUMView, crashDate: realCrashDate) rumEventOutput.write(rumEvent: rumError) @@ -225,10 +228,12 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { let rumError = RUMErrorEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: nil, application: .init(id: lastRUMView.application.id), + ciTest: nil, connectivity: lastRUMView.connectivity, context: nil, date: crashDate.timeIntervalSince1970.toInt64Milliseconds, @@ -250,6 +255,7 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { id: lastRUMView.session.id, type: .user ), + source: .ios, synthetics: nil, usr: lastRUMView.usr, view: .init( @@ -271,15 +277,18 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { let original = rumViewEvent.model let rumView = RUMViewEvent( dd: .init( + browserSdkVersion: nil, documentVersion: original.dd.documentVersion + 1, session: .init(plan: .plan1) ), application: original.application, + ciTest: nil, connectivity: original.connectivity, context: original.context, date: crashDate.timeIntervalSince1970.toInt64Milliseconds - 1, // -1ms to put the crash after view in RUM session service: original.service, session: original.session, + source: .ios, synthetics: nil, usr: original.usr, view: .init( @@ -333,24 +342,27 @@ internal struct CrashReportingWithRUMIntegration: CrashReportingIntegration { let rumView = RUMViewEvent( dd: .init( + browserSdkVersion: nil, documentVersion: 1, session: .init(plan: .plan1) ), application: .init( id: rumConfiguration.applicationID ), + ciTest: nil, connectivity: RUMConnectivity( networkInfo: crashContext.lastNetworkConnectionInfo, carrierInfo: crashContext.lastCarrierInfo ), context: nil, date: startDate.timeIntervalSince1970.toInt64Milliseconds, - service: nil, + service: rumConfiguration.common.serviceName, session: .init( hasReplay: nil, id: sessionUUID.toRUMDataFormat, type: .user ), + source: .ios, synthetics: nil, usr: crashContext.lastUserInfo.flatMap { RUMUser(userInfo: $0) }, view: .init( diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift b/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift new file mode 100644 index 0000000000..7f5d5f2378 --- /dev/null +++ b/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift @@ -0,0 +1,118 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +#if DD_SDK_ENABLE_EXPERIMENTAL_APIS +import Foundation +import WebKit + +public extension WKUserContentController { + /// Enables SDK to correlate Datadog RUM events and Logs from the WebView with native RUM session. + /// + /// If the content loaded in WebView uses Datadog Browser SDK (`v4.2.0+`) and matches specified `hosts`, web events will be correlated + /// with the RUM session from native SDK. + /// + /// - Parameter hosts: a list of hosts instrumented with Browser SDK to capture Datadog events from + func trackDatadogEvents(in hosts: Set) { + addDatadogMessageHandler(allowedWebViewHosts: hosts, hostsSanitizer: HostsSanitizer()) + } + + internal func addDatadogMessageHandler(allowedWebViewHosts: Set, hostsSanitizer: HostsSanitizing) { + let bridgeName = DatadogMessageHandler.name + + let globalRUMMonitor = Global.rum as? RUMMonitor + + var logEventConsumer: DefaultWebLogEventConsumer? = nil + if let loggingFeature = LoggingFeature.instance { + logEventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: loggingFeature.storage.writer, + internalLogsWriter: InternalMonitoringFeature.instance?.logsStorage.writer, + dateCorrector: loggingFeature.dateCorrector, + rumContextProvider: globalRUMMonitor?.contextProvider, + applicationVersion: loggingFeature.configuration.common.applicationVersion, + environment: loggingFeature.configuration.common.environment + ) + } + + var rumEventConsumer: DefaultWebRUMEventConsumer? = nil + if let rumFeature = RUMFeature.instance { + rumEventConsumer = DefaultWebRUMEventConsumer( + dataWriter: rumFeature.storage.writer, + dateCorrector: rumFeature.dateCorrector, + contextProvider: globalRUMMonitor?.contextProvider, + rumCommandSubscriber: globalRUMMonitor, + dateProvider: rumFeature.dateProvider + ) + } + + let messageHandler = DatadogMessageHandler( + eventBridge: WebEventBridge( + logEventConsumer: logEventConsumer, + rumEventConsumer: rumEventConsumer + ) + ) + add(messageHandler, name: bridgeName) + + // WebKit installs message handlers with the given name format below + // We inject a user script to forward `window.{bridgeName}` to WebKit's format + let webkitMethodName = "window.webkit.messageHandlers.\(bridgeName).postMessage" + // `WKScriptMessageHandlerWithReply` returns `Promise` and `browser-sdk` expects immediate values. + // We inject a user script to return `allowedWebViewHosts` instead of using `WKScriptMessageHandlerWithReply` + let sanitizedHosts = hostsSanitizer.sanitized( + hosts: allowedWebViewHosts, + warningMessage: "The allowed WebView host configured for Datadog SDK is not valid" + ) + let allowedWebViewHostsString = sanitizedHosts + .map { return "\"\($0)\"" } + .joined(separator: ",") + + let js = """ + window.\(bridgeName) = { + send(msg) { + \(webkitMethodName)(msg) + }, + getAllowedWebViewHosts() { + return '[\(allowedWebViewHostsString)]' + } + } + """ + addUserScript( + WKUserScript( + source: js, + injectionTime: .atDocumentStart, + forMainFrameOnly: false + ) + ) + } +} + +internal class DatadogMessageHandler: NSObject, WKScriptMessageHandler { + static let name = "DatadogEventBridge" + private let eventBridge: WebEventBridge + let queue = DispatchQueue( + label: "com.datadoghq.JSEventBridge", + target: .global(qos: .userInteractive) + ) + + init(eventBridge: WebEventBridge) { + self.eventBridge = eventBridge + } + + func userContentController( + _ userContentController: WKUserContentController, + didReceive message: WKScriptMessage + ) { + // message.body must be called within UI thread + let messageBody = message.body + queue.async { + do { + try self.eventBridge.consume(messageBody) + } catch { + userLogger.error("🔥 Web Event Error: \(error)") + } + } + } +} +#endif diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift b/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift new file mode 100644 index 0000000000..07b875ea2c --- /dev/null +++ b/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift @@ -0,0 +1,83 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal typealias JSON = [String: Any] + +internal protocol WebLogEventConsumer { + func consume(event: JSON, internalLog: Bool) throws +} + +internal protocol WebRUMEventConsumer { + func consume(event: JSON) throws +} + +internal enum WebEventError: Error, Equatable { + case dataSerialization(message: String) + case JSONDeserialization(rawJSONDescription: String) + case invalidMessage(description: String) + case missingKey(key: String) +} + +internal class WebEventBridge { + struct Constants { + static let eventTypeKey = "eventType" + static let eventKey = "event" + static let eventTypeLog = "log" + static let eventTypeInternalLog = "internal_log" + } + + private let logEventConsumer: WebLogEventConsumer? + private let rumEventConsumer: WebRUMEventConsumer? + + init(logEventConsumer: WebLogEventConsumer?, rumEventConsumer: WebRUMEventConsumer?) { + self.logEventConsumer = logEventConsumer + self.rumEventConsumer = rumEventConsumer + } + + func consume(_ anyMessage: Any) throws { + guard let message = anyMessage as? String else { + throw WebEventError.invalidMessage(description: String(describing: anyMessage)) + } + let eventJSON = try parse(message) + guard let eventType = eventJSON[Constants.eventTypeKey] as? String else { + throw WebEventError.missingKey(key: Constants.eventTypeKey) + } + guard let wrappedEvent = eventJSON[Constants.eventKey] as? JSON else { + throw WebEventError.missingKey(key: Constants.eventKey) + } + + if eventType == Constants.eventTypeLog || + eventType == Constants.eventTypeInternalLog { + if let consumer = logEventConsumer { + try consumer.consume( + event: wrappedEvent, + internalLog: (eventType == Constants.eventTypeInternalLog) + ) + } else { + userLogger.warn("A WebView log is lost because Logging is disabled in iOS SDK") + } + } else { + if let consumer = rumEventConsumer { + try consumer.consume(event: wrappedEvent) + } else { + userLogger.warn("A WebView RUM event is lost because RUM is disabled in iOS SDK") + } + } + } + + private func parse(_ message: String) throws -> JSON { + guard let data = message.data(using: .utf8) else { + throw WebEventError.dataSerialization(message: message) + } + let rawJSON = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = rawJSON as? JSON else { + throw WebEventError.JSONDeserialization(rawJSONDescription: String(describing: rawJSON)) + } + return json + } +} diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift b/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift new file mode 100644 index 0000000000..0f01e55b2c --- /dev/null +++ b/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift @@ -0,0 +1,83 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal class DefaultWebLogEventConsumer: WebLogEventConsumer { + private struct Constants { + static let logEventType = "log" + static let internalLogEventType = "internal_log" + + static let applicationIDKey = "application_id" + static let sessionIDKey = "session_id" + static let ddTagsKey = "ddtags" + static let dateKey = "date" + } + + private let userLogsWriter: Writer + private let internalLogsWriter: Writer? + private let dateCorrector: DateCorrectorType + private let rumContextProvider: RUMContextProvider? + private let applicationVersion: String + private let environment: String + + private let jsonDecoder = JSONDecoder() + + private lazy var ddTags: String = { + let versionKey = LogEventEncoder.StaticCodingKeys.applicationVersion.rawValue + let versionValue = applicationVersion + let envKey = LogEventEncoder.StaticCodingKeys.environment.rawValue + let envValue = environment + + return "\(versionKey):\(versionValue),\(envKey):\(envValue)" + }() + + init( + userLogsWriter: Writer, + internalLogsWriter: Writer?, + dateCorrector: DateCorrectorType, + rumContextProvider: RUMContextProvider?, + applicationVersion: String, + environment: String + ) { + self.userLogsWriter = userLogsWriter + self.internalLogsWriter = internalLogsWriter + self.dateCorrector = dateCorrector + self.rumContextProvider = rumContextProvider + self.applicationVersion = applicationVersion + self.environment = environment + } + + func consume(event: JSON, internalLog: Bool) throws { + var mutableEvent = event + + if let existingTags = mutableEvent[Constants.ddTagsKey] as? String, !existingTags.isEmpty { + mutableEvent[Constants.ddTagsKey] = "\(ddTags),\(existingTags)" + } else { + mutableEvent[Constants.ddTagsKey] = ddTags + } + + if let timestampInMs = mutableEvent[Constants.dateKey] as? Int { + let serverTimeOffsetInMs = dateCorrector.currentCorrection.serverTimeOffset.toInt64Milliseconds + let correctedTimestamp = Int64(timestampInMs) + serverTimeOffsetInMs + mutableEvent[Constants.dateKey] = correctedTimestamp + } + + if let context = rumContextProvider?.context { + mutableEvent[Constants.applicationIDKey] = context.rumApplicationID + mutableEvent[Constants.sessionIDKey] = context.sessionID.toRUMDataFormat + } + + let jsonData = try JSONSerialization.data(withJSONObject: mutableEvent, options: []) + let encodableEvent = try jsonDecoder.decode(CodableValue.self, from: jsonData) + + if internalLog { + internalLogsWriter?.write(value: encodableEvent) + } else { + userLogsWriter.write(value: encodableEvent) + } + } +} diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift b/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift new file mode 100644 index 0000000000..975add700d --- /dev/null +++ b/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift @@ -0,0 +1,110 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal class DefaultWebRUMEventConsumer: WebRUMEventConsumer { + private let dataWriter: Writer + private let dateCorrector: DateCorrectorType + private let contextProvider: RUMContextProvider? + private let rumCommandSubscriber: RUMCommandSubscriber? + private let dateProvider: DateProvider + + private let jsonDecoder = JSONDecoder() + + init( + dataWriter: Writer, + dateCorrector: DateCorrectorType, + contextProvider: RUMContextProvider?, + rumCommandSubscriber: RUMCommandSubscriber?, + dateProvider: DateProvider + ) { + self.dataWriter = dataWriter + self.dateCorrector = dateCorrector + self.contextProvider = contextProvider + self.rumCommandSubscriber = rumCommandSubscriber + self.dateProvider = dateProvider + } + + func consume(event: JSON) throws { + rumCommandSubscriber?.process( + command: RUMKeepSessionAliveCommand( + time: dateProvider.currentDate(), + attributes: [:] + ) + ) + let rumContext = contextProvider?.context + let mappedEvent = map(event: event, with: rumContext) + + let jsonData = try JSONSerialization.data(withJSONObject: mappedEvent, options: []) + let encodableEvent = try jsonDecoder.decode(CodableValue.self, from: jsonData) + + dataWriter.write(value: encodableEvent) + } + + private func map(event: JSON, with context: RUMContext?) -> JSON { + guard let context = context, + context.sessionID != .nullUUID else { + return event + } + + var mutableEvent = event + + if let date = mutableEvent["date"] as? Int { + let viewID = (mutableEvent["view"] as? JSON)?["id"] as? String + let serverTimeOffsetInMs = getOffsetInMs(viewID: viewID) + let correctedDate = Int64(date) + serverTimeOffsetInMs + mutableEvent["date"] = correctedDate + } + + if let context = contextProvider?.context { + if var application = mutableEvent["application"] as? JSON { + application["id"] = context.rumApplicationID + mutableEvent["application"] = application + } + if var session = mutableEvent["session"] as? JSON { + session["id"] = context.sessionID.toRUMDataFormat + mutableEvent["session"] = session + } + } + + if var dd = mutableEvent["_dd"] as? JSON, + var dd_sesion = dd["session"] as? [String: Int] { + dd_sesion["plan"] = 1 + dd["session"] = dd_sesion + mutableEvent["_dd"] = dd + } + + return mutableEvent + } + + // MARK: - Time offsets + + private typealias Offset = Int64 + private typealias ViewIDOffsetPair = (viewID: String, offset: Offset) + private var viewIDOffsetPairs = [ViewIDOffsetPair]() + + private func getOffsetInMs(viewID: String?) -> Offset { + guard let viewID = viewID else { + return 0 + } + + purgeOffsets() + let found = viewIDOffsetPairs.first { $0.viewID == viewID } + if let found = found { + return found.offset + } + let offset = dateCorrector.currentCorrection.serverTimeOffset.toInt64Milliseconds + viewIDOffsetPairs.insert((viewID: viewID, offset: offset), at: 0) + return offset + } + + private func purgeOffsets() { + while viewIDOffsetPairs.count > 3 { + _ = viewIDOffsetPairs.popLast() + } + } +} diff --git a/Sources/Datadog/Logging/Log/LogEventEncoder.swift b/Sources/Datadog/Logging/Log/LogEventEncoder.swift index 58663c2808..252c6290d9 100644 --- a/Sources/Datadog/Logging/Log/LogEventEncoder.swift +++ b/Sources/Datadog/Logging/Log/LogEventEncoder.swift @@ -89,6 +89,7 @@ internal struct LogEventEncoder { case status case message case serviceName = "service" + case environment = "env" case tags = "ddtags" // MARK: - Error diff --git a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift index e49f02bd5a..fba88de49a 100644 --- a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift +++ b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift @@ -16,6 +16,9 @@ public struct RUMViewEvent: RUMDataModel { /// Application properties public let application: Application + /// CI Visibility properties + public let ciTest: CiTest? + /// Device connectivity properties public let connectivity: RUMConnectivity? @@ -31,6 +34,9 @@ public struct RUMViewEvent: RUMDataModel { /// Session properties public let session: Session + /// The source of this event + public let source: Source? + /// Synthetics properties public let synthetics: Synthetics? @@ -46,11 +52,13 @@ public struct RUMViewEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case dd = "_dd" case application = "application" + case ciTest = "ci_test" case connectivity = "connectivity" case context = "context" case date = "date" case service = "service" case session = "session" + case source = "source" case synthetics = "synthetics" case type = "type" case usr = "usr" @@ -59,6 +67,9 @@ public struct RUMViewEvent: RUMDataModel { /// Internal properties public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + /// Version of the update of the view event public let documentVersion: Int64 @@ -69,6 +80,7 @@ public struct RUMViewEvent: RUMDataModel { public let session: Session? enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" case documentVersion = "document_version" case formatVersion = "format_version" case session = "session" @@ -101,6 +113,16 @@ public struct RUMViewEvent: RUMDataModel { } } + /// CI Visibility properties + public struct CiTest: Codable { + /// The identifier of the current CI Visibility test execution + public let testExecutionId: String + + enum CodingKeys: String, CodingKey { + case testExecutionId = "test_execution_id" + } + } + /// Session properties public struct Session: Codable { /// Whether this session has a replay @@ -122,11 +144,24 @@ public struct RUMViewEvent: RUMDataModel { public enum SessionType: String, Codable { case user = "user" case synthetics = "synthetics" + case ciTest = "ci_test" } } + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + } + /// Synthetics properties public struct Synthetics: Codable { + /// Whether the event comes from a SDK instance injected by Synthetics + public let injected: Bool? + /// The identifier of the current Synthetics test results public let resultId: String @@ -134,6 +169,7 @@ public struct RUMViewEvent: RUMDataModel { public let testId: String enum CodingKeys: String, CodingKey { + case injected = "injected" case resultId = "result_id" case testId = "test_id" } @@ -371,6 +407,9 @@ public struct RUMResourceEvent: RUMDataModel { /// Application properties public let application: Application + /// CI Visibility properties + public let ciTest: CiTest? + /// Device connectivity properties public let connectivity: RUMConnectivity? @@ -389,6 +428,9 @@ public struct RUMResourceEvent: RUMDataModel { /// Session properties public let session: Session + /// The source of this event + public let source: Source? + /// Synthetics properties public let synthetics: Synthetics? @@ -405,12 +447,14 @@ public struct RUMResourceEvent: RUMDataModel { case dd = "_dd" case action = "action" case application = "application" + case ciTest = "ci_test" case connectivity = "connectivity" case context = "context" case date = "date" case resource = "resource" case service = "service" case session = "session" + case source = "source" case synthetics = "synthetics" case type = "type" case usr = "usr" @@ -419,6 +463,9 @@ public struct RUMResourceEvent: RUMDataModel { /// Internal properties public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + /// Version of the RUM event format public let formatVersion: Int64 = 2 @@ -432,6 +479,7 @@ public struct RUMResourceEvent: RUMDataModel { public let traceId: String? enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" case formatVersion = "format_version" case session = "session" case spanId = "span_id" @@ -475,6 +523,16 @@ public struct RUMResourceEvent: RUMDataModel { } } + /// CI Visibility properties + public struct CiTest: Codable { + /// The identifier of the current CI Visibility test execution + public let testExecutionId: String + + enum CodingKeys: String, CodingKey { + case testExecutionId = "test_execution_id" + } + } + /// Resource properties public struct Resource: Codable { /// Connect phase properties @@ -693,11 +751,24 @@ public struct RUMResourceEvent: RUMDataModel { public enum SessionType: String, Codable { case user = "user" case synthetics = "synthetics" + case ciTest = "ci_test" } } + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + } + /// Synthetics properties public struct Synthetics: Codable { + /// Whether the event comes from a SDK instance injected by Synthetics + public let injected: Bool? + /// The identifier of the current Synthetics test results public let resultId: String @@ -705,6 +776,7 @@ public struct RUMResourceEvent: RUMDataModel { public let testId: String enum CodingKeys: String, CodingKey { + case injected = "injected" case resultId = "result_id" case testId = "test_id" } @@ -744,6 +816,9 @@ public struct RUMActionEvent: RUMDataModel { /// Application properties public let application: Application + /// CI Visibility properties + public let ciTest: CiTest? + /// Device connectivity properties public let connectivity: RUMConnectivity? @@ -759,6 +834,9 @@ public struct RUMActionEvent: RUMDataModel { /// Session properties public let session: Session + /// The source of this event + public let source: Source? + /// Synthetics properties public let synthetics: Synthetics? @@ -775,11 +853,13 @@ public struct RUMActionEvent: RUMDataModel { case dd = "_dd" case action = "action" case application = "application" + case ciTest = "ci_test" case connectivity = "connectivity" case context = "context" case date = "date" case service = "service" case session = "session" + case source = "source" case synthetics = "synthetics" case type = "type" case usr = "usr" @@ -788,6 +868,9 @@ public struct RUMActionEvent: RUMDataModel { /// Internal properties public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + /// Version of the RUM event format public let formatVersion: Int64 = 2 @@ -795,6 +878,7 @@ public struct RUMActionEvent: RUMDataModel { public let session: Session? enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" case formatVersion = "format_version" case session = "session" } @@ -925,6 +1009,16 @@ public struct RUMActionEvent: RUMDataModel { } } + /// CI Visibility properties + public struct CiTest: Codable { + /// The identifier of the current CI Visibility test execution + public let testExecutionId: String + + enum CodingKeys: String, CodingKey { + case testExecutionId = "test_execution_id" + } + } + /// Session properties public struct Session: Codable { /// Whether this session has a replay @@ -946,11 +1040,24 @@ public struct RUMActionEvent: RUMDataModel { public enum SessionType: String, Codable { case user = "user" case synthetics = "synthetics" + case ciTest = "ci_test" } } + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + } + /// Synthetics properties public struct Synthetics: Codable { + /// Whether the event comes from a SDK instance injected by Synthetics + public let injected: Bool? + /// The identifier of the current Synthetics test results public let resultId: String @@ -958,6 +1065,7 @@ public struct RUMActionEvent: RUMDataModel { public let testId: String enum CodingKeys: String, CodingKey { + case injected = "injected" case resultId = "result_id" case testId = "test_id" } @@ -1001,6 +1109,9 @@ public struct RUMErrorEvent: RUMDataModel { /// Application properties public let application: Application + /// CI Visibility properties + public let ciTest: CiTest? + /// Device connectivity properties public let connectivity: RUMConnectivity? @@ -1019,6 +1130,9 @@ public struct RUMErrorEvent: RUMDataModel { /// Session properties public let session: Session + /// The source of this event + public let source: Source? + /// Synthetics properties public let synthetics: Synthetics? @@ -1035,12 +1149,14 @@ public struct RUMErrorEvent: RUMDataModel { case dd = "_dd" case action = "action" case application = "application" + case ciTest = "ci_test" case connectivity = "connectivity" case context = "context" case date = "date" case error = "error" case service = "service" case session = "session" + case source = "source" case synthetics = "synthetics" case type = "type" case usr = "usr" @@ -1049,6 +1165,9 @@ public struct RUMErrorEvent: RUMDataModel { /// Internal properties public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + /// Version of the RUM event format public let formatVersion: Int64 = 2 @@ -1056,6 +1175,7 @@ public struct RUMErrorEvent: RUMDataModel { public let session: Session? enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" case formatVersion = "format_version" case session = "session" } @@ -1097,6 +1217,16 @@ public struct RUMErrorEvent: RUMDataModel { } } + /// CI Visibility properties + public struct CiTest: Codable { + /// The identifier of the current CI Visibility test execution + public let testExecutionId: String + + enum CodingKeys: String, CodingKey { + case testExecutionId = "test_execution_id" + } + } + /// Error properties public struct Error: Codable { /// Whether the error has been handled manually in the source code or not @@ -1223,6 +1353,7 @@ public struct RUMErrorEvent: RUMDataModel { case browser = "browser" case ios = "ios" case reactNative = "react-native" + case flutter = "flutter" } } @@ -1247,11 +1378,24 @@ public struct RUMErrorEvent: RUMDataModel { public enum SessionType: String, Codable { case user = "user" case synthetics = "synthetics" + case ciTest = "ci_test" } } + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + } + /// Synthetics properties public struct Synthetics: Codable { + /// Whether the event comes from a SDK instance injected by Synthetics + public let injected: Bool? + /// The identifier of the current Synthetics test results public let resultId: String @@ -1259,6 +1403,7 @@ public struct RUMErrorEvent: RUMDataModel { public let testId: String enum CodingKeys: String, CodingKey { + case injected = "injected" case resultId = "result_id" case testId = "test_id" } @@ -1302,6 +1447,9 @@ public struct RUMLongTaskEvent: RUMDataModel { /// Application properties public let application: Application + /// CI Visibility properties + public let ciTest: CiTest? + /// Device connectivity properties public let connectivity: RUMConnectivity? @@ -1320,6 +1468,9 @@ public struct RUMLongTaskEvent: RUMDataModel { /// Session properties public let session: Session + /// The source of this event + public let source: Source? + /// Synthetics properties public let synthetics: Synthetics? @@ -1336,12 +1487,14 @@ public struct RUMLongTaskEvent: RUMDataModel { case dd = "_dd" case action = "action" case application = "application" + case ciTest = "ci_test" case connectivity = "connectivity" case context = "context" case date = "date" case longTask = "long_task" case service = "service" case session = "session" + case source = "source" case synthetics = "synthetics" case type = "type" case usr = "usr" @@ -1350,6 +1503,9 @@ public struct RUMLongTaskEvent: RUMDataModel { /// Internal properties public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + /// Version of the RUM event format public let formatVersion: Int64 = 2 @@ -1357,6 +1513,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let session: Session? enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" case formatVersion = "format_version" case session = "session" } @@ -1398,6 +1555,16 @@ public struct RUMLongTaskEvent: RUMDataModel { } } + /// CI Visibility properties + public struct CiTest: Codable { + /// The identifier of the current CI Visibility test execution + public let testExecutionId: String + + enum CodingKeys: String, CodingKey { + case testExecutionId = "test_execution_id" + } + } + /// Long Task properties public struct LongTask: Codable { /// Duration in ns of the long task @@ -1437,11 +1604,24 @@ public struct RUMLongTaskEvent: RUMDataModel { public enum SessionType: String, Codable { case user = "user" case synthetics = "synthetics" + case ciTest = "ci_test" } } + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + } + /// Synthetics properties public struct Synthetics: Codable { + /// Whether the event comes from a SDK instance injected by Synthetics + public let injected: Bool? + /// The identifier of the current Synthetics test results public let resultId: String @@ -1449,6 +1629,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let testId: String enum CodingKeys: String, CodingKey { + case injected = "injected" case resultId = "result_id" case testId = "test_id" } @@ -1640,4 +1821,4 @@ public enum RUMMethod: String, Codable { case patch = "PATCH" } -// Generated from https://github.com/DataDog/rum-events-format/tree/9c135e77bb1da61ebbb6b2fb3b39e156d5120a8e +// Generated from https://github.com/DataDog/rum-events-format/tree/114c173caac5ea15446a157b666acbab05431361 diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index e5f0047960..2b7072090b 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -305,3 +305,13 @@ internal struct RUMAddLongTaskCommand: RUMCommand { let duration: TimeInterval } + +// MARK: - RUM Web Events related commands + +/// RUM Events received from WebView should keep the active session alive, therefore they fire this command to do so. (ref: RUMM-1793) +internal struct RUMKeepSessionAliveCommand: RUMCommand { + let canStartBackgroundView = false + let canStartApplicationLaunchView = false + var time: Date + var attributes: [AttributeKey: AttributeValue] +} diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 46d0a2dd55..56c1bd9344 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -14,6 +14,7 @@ internal struct RUMScopeDependencies { let userInfoProvider: RUMUserInfoProvider let launchTimeProvider: LaunchTimeProviderType let connectivityInfoProvider: RUMConnectivityInfoProvider + let serviceName: String let eventBuilder: RUMEventBuilder let eventOutput: RUMEventOutput let rumUUIDGenerator: RUMUUIDGenerator diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift index 15926dbc6e..c18d89b139 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift @@ -130,6 +130,7 @@ internal class RUMResourceScope: RUMScope { let eventData = RUMResourceEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1), spanId: spanId, traceId: traceId @@ -138,6 +139,7 @@ internal class RUMResourceScope: RUMScope { .init(id: rumUUID.toRUMDataFormat) }, application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: resourceStartTime).timeIntervalSince1970.toInt64Milliseconds, @@ -187,8 +189,9 @@ internal class RUMResourceScope: RUMScope { type: resourceType, url: resourceURL ), - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( @@ -211,12 +214,14 @@ internal class RUMResourceScope: RUMScope { let eventData = RUMErrorEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: context.activeUserActionID.flatMap { rumUUID in .init(id: rumUUID.toRUMDataFormat) }, application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: command.time).timeIntervalSince1970.toInt64Milliseconds, @@ -237,8 +242,9 @@ internal class RUMResourceScope: RUMScope { stack: command.stack, type: command.errorType ), - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index d460da5f31..d9b1022196 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -146,14 +146,16 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { case .handleInBackgroundView where command.canStartBackgroundView: startBackgroundView(on: command) default: - // As no view scope will handle this command, warn the user on dropping it - userLogger.warn( - """ - \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. - """ - ) + if !(command is RUMKeepSessionAliveCommand) { // it is expected to receive 'keep alive' while no active view (when tracking WebView events) + // As no view scope will handle this command, warn the user on dropping it. + userLogger.warn( + """ + \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the + DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using + the RumMonitor.startView() and RumMonitor.stopView() methods. + """ + ) + } } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift index 3f8822eec4..33cb2dda82 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift @@ -134,6 +134,7 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider { let eventData = RUMActionEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: .init( @@ -147,11 +148,13 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider { type: actionType.toRUMDataFormat ), application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: actionStartTime).timeIntervalSince1970.toInt64Milliseconds, - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 403478b6bc..d1134f116a 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -305,6 +305,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { private func sendApplicationStartAction() -> Bool { let eventData = RUMActionEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: .init( @@ -318,11 +319,13 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { type: .applicationStart ), application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: viewStartTime).timeIntervalSince1970.toInt64Milliseconds, - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( @@ -356,15 +359,18 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { let eventData = RUMViewEvent( dd: .init( + browserSdkVersion: nil, documentVersion: version.toInt64, session: .init(plan: .plan1) ), application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: viewStartTime).timeIntervalSince1970.toInt64Milliseconds, - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( @@ -420,12 +426,14 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { let eventData = RUMErrorEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: context.activeUserActionID.flatMap { rumUUID in .init(id: rumUUID.toRUMDataFormat) }, application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: command.time).timeIntervalSince1970.toInt64Milliseconds, @@ -441,8 +449,9 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { stack: command.stack, type: command.type ), - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( @@ -468,15 +477,20 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { let isFrozenFrame = taskDurationInNs > Constants.frozenFrameThresholdInNs let eventData = RUMLongTaskEvent( - dd: .init(session: .init(plan: .plan1)), + dd: .init( + browserSdkVersion: nil, + session: .init(plan: .plan1) + ), action: context.activeUserActionID.flatMap { RUMLongTaskEvent.Action(id: $0.toRUMDataFormat) }, application: .init(id: context.rumApplicationID), + ciTest: nil, connectivity: dependencies.connectivityInfoProvider.current, context: .init(contextInfo: attributes), date: dateCorrection.applying(to: command.time - command.duration).timeIntervalSince1970.toInt64Milliseconds, longTask: .init(duration: taskDurationInNs, id: nil, isFrozenFrame: isFrozenFrame), - service: nil, + service: dependencies.serviceName, session: .init(hasReplay: nil, id: context.sessionID.toRUMDataFormat, type: .user), + source: .ios, synthetics: nil, usr: dependencies.userInfoProvider.current, view: .init( diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index bf4a5a2c98..765b0ce728 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -185,6 +185,7 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { networkConnectionInfoProvider: rumFeature.networkConnectionInfoProvider, carrierInfoProvider: rumFeature.carrierInfoProvider ), + serviceName: rumFeature.configuration.common.serviceName, eventBuilder: RUMEventBuilder( eventsMapper: rumFeature.eventsMapper ), diff --git a/Sources/DatadogCrashReporting/PLCrashReporterIntegration/CrashReport.swift b/Sources/DatadogCrashReporting/PLCrashReporterIntegration/CrashReport.swift index 3cc41cd13f..25510743e1 100644 --- a/Sources/DatadogCrashReporting/PLCrashReporterIntegration/CrashReport.swift +++ b/Sources/DatadogCrashReporting/PLCrashReporterIntegration/CrashReport.swift @@ -91,7 +91,7 @@ internal struct BinaryImageInfo { } /// The UUID of this image. - var uuid: String + var uuid: String? /// The name of this image (referenced by "library name" in the stack frame). var imageName: String /// If its a system library image. @@ -232,14 +232,12 @@ extension ThreadInfo { extension BinaryImageInfo { init?(from imageInfo: PLCrashReportBinaryImageInfo) { - guard let imagePath = imageInfo.imageName, - let imageUUID = imageInfo.hasImageUUID ? imageInfo.imageUUID : nil else { - // We can drop this image as it won't be useful for symbolication - both - // "image name" and "uuid" are necessary. + guard let imagePath = imageInfo.imageName else { + // We can drop this image as it won't be useful for symbolication return nil } - self.uuid = imageUUID + self.uuid = imageInfo.imageUUID self.imageName = URL(fileURLWithPath: imagePath).lastPathComponent #if targetEnvironment(simulator) @@ -303,18 +301,17 @@ extension BinaryImageInfo.CodeType { extension StackFrame { init(from stackFrame: PLCrashReportStackFrameInfo, number: Int, in crashReport: PLCrashReport) { - if let image = crashReport.image(forAddress: stackFrame.instructionPointer), - let imageInfo = BinaryImageInfo(from: image) { - self.libraryName = imageInfo.imageName - self.libraryBaseAddress = imageInfo.imageBaseAddress - } else { - // Without "library name" and its "base address" symbolication will not be possible, - // but the presence of this frame in the stack will be still relevant. - self.libraryName = nil - self.libraryBaseAddress = nil - } - self.number = number self.instructionPointer = stackFrame.instructionPointer + + // Without "library name" and its "base address" symbolication will not be possible, + // but the presence of this frame in the stack will be still relevant. + let image = crashReport.image(forAddress: stackFrame.instructionPointer) + + self.libraryBaseAddress = image?.imageBaseAddress + + if let imagePath = image?.imageName { + self.libraryName = URL(fileURLWithPath: imagePath).lastPathComponent + } } } diff --git a/Sources/DatadogCrashReporting/PLCrashReporterIntegration/DDCrashReportExporter.swift b/Sources/DatadogCrashReporting/PLCrashReporterIntegration/DDCrashReportExporter.swift index d4737c8ae2..4e243f8c1f 100644 --- a/Sources/DatadogCrashReporting/PLCrashReporterIntegration/DDCrashReportExporter.swift +++ b/Sources/DatadogCrashReporting/PLCrashReporterIntegration/DDCrashReportExporter.swift @@ -150,7 +150,7 @@ internal struct DDCrashReportExporter { return DDCrashReport.BinaryImage( libraryName: image.imageName, - uuid: image.uuid, + uuid: image.uuid ?? unavailable, architecture: image.codeType?.architectureName ?? unavailable, isSystemLibrary: image.isSystemImage, loadAddress: loadAddressHex, diff --git a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift index bdbe834c8e..001b65d7db 100644 --- a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift +++ b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift @@ -28,6 +28,10 @@ public class DDRUMViewEvent: NSObject { DDRUMViewEventApplication(root: root) } + @objc public var ciTest: DDRUMViewEventCiTest? { + root.swiftModel.ciTest != nil ? DDRUMViewEventCiTest(root: root) : nil + } + @objc public var connectivity: DDRUMViewEventRUMConnectivity? { root.swiftModel.connectivity != nil ? DDRUMViewEventRUMConnectivity(root: root) : nil } @@ -48,6 +52,10 @@ public class DDRUMViewEvent: NSObject { DDRUMViewEventSession(root: root) } + @objc public var source: DDRUMViewEventSource { + .init(swift: root.swiftModel.source) + } + @objc public var synthetics: DDRUMViewEventSynthetics? { root.swiftModel.synthetics != nil ? DDRUMViewEventSynthetics(root: root) : nil } @@ -73,6 +81,10 @@ public class DDRUMViewEventDD: NSObject { self.root = root } + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + @objc public var documentVersion: NSNumber { root.swiftModel.dd.documentVersion as NSNumber } @@ -132,6 +144,19 @@ public class DDRUMViewEventApplication: NSObject { } } +@objc +public class DDRUMViewEventCiTest: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + @objc public class DDRUMViewEventRUMConnectivity: NSObject { internal let root: DDRUMViewEvent @@ -274,6 +299,7 @@ public enum DDRUMViewEventSessionSessionType: Int { switch swift { case .user: self = .user case .synthetics: self = .synthetics + case .ciTest: self = .ciTest } } @@ -281,11 +307,45 @@ public enum DDRUMViewEventSessionSessionType: Int { switch self { case .user: return .user case .synthetics: return .synthetics + case .ciTest: return .ciTest } } case user case synthetics + case ciTest +} + +@objc +public enum DDRUMViewEventSource: Int { + internal init(swift: RUMViewEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + } + } + + internal var toSwift: RUMViewEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative } @objc @@ -296,6 +356,10 @@ public class DDRUMViewEventSynthetics: NSObject { self.root = root } + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + @objc public var resultId: String { root.swiftModel.synthetics!.resultId } @@ -628,6 +692,10 @@ public class DDRUMResourceEvent: NSObject { DDRUMResourceEventApplication(root: root) } + @objc public var ciTest: DDRUMResourceEventCiTest? { + root.swiftModel.ciTest != nil ? DDRUMResourceEventCiTest(root: root) : nil + } + @objc public var connectivity: DDRUMResourceEventRUMConnectivity? { root.swiftModel.connectivity != nil ? DDRUMResourceEventRUMConnectivity(root: root) : nil } @@ -652,6 +720,10 @@ public class DDRUMResourceEvent: NSObject { DDRUMResourceEventSession(root: root) } + @objc public var source: DDRUMResourceEventSource { + .init(swift: root.swiftModel.source) + } + @objc public var synthetics: DDRUMResourceEventSynthetics? { root.swiftModel.synthetics != nil ? DDRUMResourceEventSynthetics(root: root) : nil } @@ -677,6 +749,10 @@ public class DDRUMResourceEventDD: NSObject { self.root = root } + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + @objc public var formatVersion: NSNumber { root.swiftModel.dd.formatVersion as NSNumber } @@ -753,6 +829,19 @@ public class DDRUMResourceEventApplication: NSObject { } } +@objc +public class DDRUMResourceEventCiTest: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + @objc public class DDRUMResourceEventRUMConnectivity: NSObject { internal let root: DDRUMResourceEvent @@ -1225,6 +1314,7 @@ public enum DDRUMResourceEventSessionSessionType: Int { switch swift { case .user: self = .user case .synthetics: self = .synthetics + case .ciTest: self = .ciTest } } @@ -1232,11 +1322,45 @@ public enum DDRUMResourceEventSessionSessionType: Int { switch self { case .user: return .user case .synthetics: return .synthetics + case .ciTest: return .ciTest } } case user case synthetics + case ciTest +} + +@objc +public enum DDRUMResourceEventSource: Int { + internal init(swift: RUMResourceEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + } + } + + internal var toSwift: RUMResourceEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative } @objc @@ -1247,6 +1371,10 @@ public class DDRUMResourceEventSynthetics: NSObject { self.root = root } + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + @objc public var resultId: String { root.swiftModel.synthetics!.resultId } @@ -1330,6 +1458,10 @@ public class DDRUMActionEvent: NSObject { DDRUMActionEventApplication(root: root) } + @objc public var ciTest: DDRUMActionEventCiTest? { + root.swiftModel.ciTest != nil ? DDRUMActionEventCiTest(root: root) : nil + } + @objc public var connectivity: DDRUMActionEventRUMConnectivity? { root.swiftModel.connectivity != nil ? DDRUMActionEventRUMConnectivity(root: root) : nil } @@ -1350,6 +1482,10 @@ public class DDRUMActionEvent: NSObject { DDRUMActionEventSession(root: root) } + @objc public var source: DDRUMActionEventSource { + .init(swift: root.swiftModel.source) + } + @objc public var synthetics: DDRUMActionEventSynthetics? { root.swiftModel.synthetics != nil ? DDRUMActionEventSynthetics(root: root) : nil } @@ -1375,6 +1511,10 @@ public class DDRUMActionEventDD: NSObject { self.root = root } + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + @objc public var formatVersion: NSNumber { root.swiftModel.dd.formatVersion as NSNumber } @@ -1572,6 +1712,19 @@ public class DDRUMActionEventApplication: NSObject { } } +@objc +public class DDRUMActionEventCiTest: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + @objc public class DDRUMActionEventRUMConnectivity: NSObject { internal let root: DDRUMActionEvent @@ -1714,6 +1867,7 @@ public enum DDRUMActionEventSessionSessionType: Int { switch swift { case .user: self = .user case .synthetics: self = .synthetics + case .ciTest: self = .ciTest } } @@ -1721,11 +1875,45 @@ public enum DDRUMActionEventSessionSessionType: Int { switch self { case .user: return .user case .synthetics: return .synthetics + case .ciTest: return .ciTest } } case user case synthetics + case ciTest +} + +@objc +public enum DDRUMActionEventSource: Int { + internal init(swift: RUMActionEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + } + } + + internal var toSwift: RUMActionEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative } @objc @@ -1736,6 +1924,10 @@ public class DDRUMActionEventSynthetics: NSObject { self.root = root } + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + @objc public var resultId: String { root.swiftModel.synthetics!.resultId } @@ -1823,6 +2015,10 @@ public class DDRUMErrorEvent: NSObject { DDRUMErrorEventApplication(root: root) } + @objc public var ciTest: DDRUMErrorEventCiTest? { + root.swiftModel.ciTest != nil ? DDRUMErrorEventCiTest(root: root) : nil + } + @objc public var connectivity: DDRUMErrorEventRUMConnectivity? { root.swiftModel.connectivity != nil ? DDRUMErrorEventRUMConnectivity(root: root) : nil } @@ -1847,6 +2043,10 @@ public class DDRUMErrorEvent: NSObject { DDRUMErrorEventSession(root: root) } + @objc public var source: DDRUMErrorEventSource { + .init(swift: root.swiftModel.source) + } + @objc public var synthetics: DDRUMErrorEventSynthetics? { root.swiftModel.synthetics != nil ? DDRUMErrorEventSynthetics(root: root) : nil } @@ -1872,6 +2072,10 @@ public class DDRUMErrorEventDD: NSObject { self.root = root } + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + @objc public var formatVersion: NSNumber { root.swiftModel.dd.formatVersion as NSNumber } @@ -1940,6 +2144,19 @@ public class DDRUMErrorEventApplication: NSObject { } } +@objc +public class DDRUMErrorEventCiTest: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + @objc public class DDRUMErrorEventRUMConnectivity: NSObject { internal let root: DDRUMErrorEvent @@ -2311,6 +2528,7 @@ public enum DDRUMErrorEventErrorSourceType: Int { case .browser?: self = .browser case .ios?: self = .ios case .reactNative?: self = .reactNative + case .flutter?: self = .flutter } } @@ -2321,6 +2539,7 @@ public enum DDRUMErrorEventErrorSourceType: Int { case .browser: return .browser case .ios: return .ios case .reactNative: return .reactNative + case .flutter: return .flutter } } @@ -2329,6 +2548,7 @@ public enum DDRUMErrorEventErrorSourceType: Int { case browser case ios case reactNative + case flutter } @objc @@ -2358,6 +2578,7 @@ public enum DDRUMErrorEventSessionSessionType: Int { switch swift { case .user: self = .user case .synthetics: self = .synthetics + case .ciTest: self = .ciTest } } @@ -2365,11 +2586,45 @@ public enum DDRUMErrorEventSessionSessionType: Int { switch self { case .user: return .user case .synthetics: return .synthetics + case .ciTest: return .ciTest } } case user case synthetics + case ciTest +} + +@objc +public enum DDRUMErrorEventSource: Int { + internal init(swift: RUMErrorEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + } + } + + internal var toSwift: RUMErrorEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative } @objc @@ -2380,6 +2635,10 @@ public class DDRUMErrorEventSynthetics: NSObject { self.root = root } + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + @objc public var resultId: String { root.swiftModel.synthetics!.resultId } @@ -2467,6 +2726,10 @@ public class DDRUMLongTaskEvent: NSObject { DDRUMLongTaskEventApplication(root: root) } + @objc public var ciTest: DDRUMLongTaskEventCiTest? { + root.swiftModel.ciTest != nil ? DDRUMLongTaskEventCiTest(root: root) : nil + } + @objc public var connectivity: DDRUMLongTaskEventRUMConnectivity? { root.swiftModel.connectivity != nil ? DDRUMLongTaskEventRUMConnectivity(root: root) : nil } @@ -2491,6 +2754,10 @@ public class DDRUMLongTaskEvent: NSObject { DDRUMLongTaskEventSession(root: root) } + @objc public var source: DDRUMLongTaskEventSource { + .init(swift: root.swiftModel.source) + } + @objc public var synthetics: DDRUMLongTaskEventSynthetics? { root.swiftModel.synthetics != nil ? DDRUMLongTaskEventSynthetics(root: root) : nil } @@ -2516,6 +2783,10 @@ public class DDRUMLongTaskEventDD: NSObject { self.root = root } + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + @objc public var formatVersion: NSNumber { root.swiftModel.dd.formatVersion as NSNumber } @@ -2584,6 +2855,19 @@ public class DDRUMLongTaskEventApplication: NSObject { } } +@objc +public class DDRUMLongTaskEventCiTest: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + @objc public class DDRUMLongTaskEventRUMConnectivity: NSObject { internal let root: DDRUMLongTaskEvent @@ -2747,6 +3031,7 @@ public enum DDRUMLongTaskEventSessionSessionType: Int { switch swift { case .user: self = .user case .synthetics: self = .synthetics + case .ciTest: self = .ciTest } } @@ -2754,11 +3039,45 @@ public enum DDRUMLongTaskEventSessionSessionType: Int { switch self { case .user: return .user case .synthetics: return .synthetics + case .ciTest: return .ciTest } } case user case synthetics + case ciTest +} + +@objc +public enum DDRUMLongTaskEventSource: Int { + internal init(swift: RUMLongTaskEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + } + } + + internal var toSwift: RUMLongTaskEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative } @objc @@ -2769,6 +3088,10 @@ public class DDRUMLongTaskEventSynthetics: NSObject { self.root = root } + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + @objc public var resultId: String { root.swiftModel.synthetics!.resultId } @@ -2833,4 +3156,4 @@ public class DDRUMLongTaskEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/9c135e77bb1da61ebbb6b2fb3b39e156d5120a8e +// Generated from https://github.com/DataDog/rum-events-format/tree/114c173caac5ea15446a157b666acbab05431361 diff --git a/Tests/DatadogCrashReportingTests/Mocks.swift b/Tests/DatadogCrashReportingTests/Mocks.swift index 849926897d..de2b8928ed 100644 --- a/Tests/DatadogCrashReportingTests/Mocks.swift +++ b/Tests/DatadogCrashReportingTests/Mocks.swift @@ -252,7 +252,7 @@ extension ThreadInfo { extension BinaryImageInfo { static func mockWith( - uuid: String = .mockAny(), + uuid: String? = .mockAny(), imageName: String = .mockAny(), isSystemImage: Bool = .random(), architectureName: String? = .mockAny(), diff --git a/Tests/DatadogCrashReportingTests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift b/Tests/DatadogCrashReportingTests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift index 9641a0df66..31ac798ab3 100644 --- a/Tests/DatadogCrashReportingTests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift +++ b/Tests/DatadogCrashReportingTests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift @@ -275,6 +275,17 @@ class DDCrashReportExporterTests: XCTestCase { } } + func testExportingBinaryImageWhenUUIDIsUnavailable() { + // Given + crashReport.binaryImages = [.mockWith(uuid: nil)] + + // When + let exportedImages = exporter.export(crashReport).binaryImages + + // Then + XCTAssertEqual(exportedImages.first?.uuid, "???") + } + func testExportingBinaryImageAddressRange() throws { let randomImageLoadAddress: UInt64 = .mockRandom() let randomImageSize: UInt64 = .mockRandom() diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift index d987eaa27e..96451dd125 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift @@ -60,14 +60,16 @@ extension RUMCommonAsserts { extension RUMSessionMatcher { /// Retrieves single RUM Session from given `requests`. - class func singleSession(from requests: [HTTPServerMock.Request]) throws -> RUMSessionMatcher? { - return try sessions(maxCount: 1, from: requests).first + /// - Parameter eventsPatch: optional transformation to apply on each event within the payload before instantiating matcher (default: `nil`) + class func singleSession(from requests: [HTTPServerMock.Request], eventsPatch: ((Data) throws -> Data)? = nil) throws -> RUMSessionMatcher? { + return try sessions(maxCount: 1, from: requests, eventsPatch: eventsPatch).first } /// Retrieves `maxCount` RUM Sessions from given `requests`. - class func sessions(maxCount: Int, from requests: [HTTPServerMock.Request]) throws -> [RUMSessionMatcher] { + /// - Parameter eventsPatch: optional transformation to apply on each event within the payload before instantiating matcher (default: `nil`) + class func sessions(maxCount: Int, from requests: [HTTPServerMock.Request], eventsPatch: ((Data) throws -> Data)? = nil) throws -> [RUMSessionMatcher] { let eventMatchers = try requests - .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody) } + .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody, eventsPatch: eventsPatch) } let sessionMatchers = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers) if sessionMatchers.count > maxCount { diff --git a/Tests/DatadogIntegrationTests/Scenarios/WebView/WebViewScenarioTest.swift b/Tests/DatadogIntegrationTests/Scenarios/WebView/WebViewScenarioTest.swift new file mode 100644 index 0000000000..f3402a48ae --- /dev/null +++ b/Tests/DatadogIntegrationTests/Scenarios/WebView/WebViewScenarioTest.swift @@ -0,0 +1,105 @@ +/* + * 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 HTTPServerMock +import XCTest + +class WebViewScenarioTest: IntegrationTests, RUMCommonAsserts { + /// In this test, the app opens a WebView which loads Browser SDK instrumented content. + /// The iOS SDK should capture all RUM events and Logs produced by Browser SDK. + func testWebViewEventsScenario() throws { + // Server session recording RUM events send to `HTTPServerMock`. + let rumServerSession = server.obtainUniqueRecordingSession() + // Server session recording Logs send to `HTTPServerMock`. + let loggingServerSession = server.obtainUniqueRecordingSession() + + let app = ExampleApplication() + app.launchWith( + testScenarioClassName: "WebViewTrackingScenario", + serverConfiguration: HTTPServerMockConfiguration( + logsEndpoint: loggingServerSession.recordingURL, + rumEndpoint: rumServerSession.recordingURL + ) + ) + + // Get single RUM Session with expected number of View visits + let recordedRUMRequests = try rumServerSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + try RUMSessionMatcher.singleSession(from: requests, eventsPatch: patchBrowserResourceEvents)?.viewVisits.count == 2 + } + assertRUM(requests: recordedRUMRequests) + + let session = try XCTUnwrap(RUMSessionMatcher.singleSession(from: recordedRUMRequests, eventsPatch: patchBrowserResourceEvents)) + XCTAssertEqual(session.viewVisits.count, 2, "There should be 2 RUM views - one native and one received from Browser SDK") + + // Check iOS SDK events: + let nativeView = session.viewVisits[0] + XCTAssertEqual(nativeView.name, "Example.WebViewTrackingFixtureViewController") + XCTAssertEqual(nativeView.path, "Example.WebViewTrackingFixtureViewController") + + nativeView.viewEvents.forEach { nativeViewEvent in + XCTAssertEqual(nativeViewEvent.source, .ios) + } + XCTAssertEqual(nativeView.actionEvents.count, 2, "It should track 2 native actions") + + // Check Browser SDK events: + let expectedBrowserServiceName = "shopist-web-ui" + let expectedBrowserRUMApplicationID = nativeView.viewEvents[0].application.id + let expectedBrowserSessionID = nativeView.viewEvents[0].session.id + + let browserView = session.viewVisits[1] + XCTAssertNil(browserView.name, "Browser views should have no `name`") + XCTAssertEqual(browserView.path, "https://shopist.io/") + + browserView.viewEvents.forEach { browserViewEvent in + XCTAssertEqual(browserViewEvent.application.id, expectedBrowserRUMApplicationID, "Webview events should use iOS SDK application ID") + XCTAssertEqual(browserViewEvent.session.id, expectedBrowserSessionID, "Webview events should use iOS SDK session ID") + XCTAssertEqual(browserViewEvent.service, expectedBrowserServiceName, "Webview events should use Browser SDK `service`") + XCTAssertEqual(browserViewEvent.source, .browser, "Webview events should use Browser SDK `source`") + } + XCTAssertGreaterThan(browserView.resourceEvents.count, 0, "It should track some Webview resources") + browserView.resourceEvents.forEach { browserResourceEvent in + XCTAssertEqual(browserResourceEvent.application.id, expectedBrowserRUMApplicationID, "Webview events should use iOS SDK application ID") + XCTAssertEqual(browserResourceEvent.session.id, expectedBrowserSessionID, "Webview events should use iOS SDK session ID") + XCTAssertEqual(browserResourceEvent.service, expectedBrowserServiceName, "Webview events should use Browser SDK `service`") + XCTAssertEqual(browserResourceEvent.source, .browser, "Webview events should use Browser SDK `source`") + } + + // Get `LogMatchers` + let recordedRequests = try loggingServerSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + try LogMatcher.from(requests: requests).count >= 1 // get at least one log + } + let logMatchers = try LogMatcher.from(requests: recordedRequests) + + let browserLog = logMatchers[0] + browserLog.assertServiceName(equals: expectedBrowserServiceName) + browserLog.assertAttributes(equal: [ + "application_id": expectedBrowserRUMApplicationID, + "session_id": expectedBrowserSessionID, + ]) + } +} + +/// Patch applied to RUM resource events received from Browser SDK. +/// +/// Browser SDK v4.1.0 uses lowercase string values for `resource.method` field, whereas RUM format schema +/// defines it as uppercase string (e.g. `"get"` instead of `"GET"`). Here we patch `resource.method` to be +/// uppercased, so it can be read and validated in `RUMEventMatcher`. +/// +/// This can be removed once `resource.method` is fixed in future Browser SDK version. +private func patchBrowserResourceEvents(_ data: Data) throws -> Data { + var json = try data.toJSONObject() + + if let eventType = json["type"] as? String, + eventType == "resource", + var resource = json["resource"] as? [String: Any], + let method = resource["method"] as? String { + resource["method"] = method.uppercased() + json["resource"] = resource + } + + return try JSONSerialization.data(withJSONObject: json, options: []) +} diff --git a/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift b/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift index 8ed9807ead..5127fed9f8 100644 --- a/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift +++ b/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift @@ -739,71 +739,26 @@ class FeaturesConfigurationTests: XCTestCase { ) } - func testWhenSomeOfTheFirstPartyHostsAreMistaken_itPrintsWarningsAndDoesSanitization() throws { - let printFunction = PrintFunctionMock() - consolePrint = printFunction.print - defer { consolePrint = { print($0) } } - + func testWhenFirstPartyHostsAreProvided_itPassesThemToSanitizer() throws { // When let firstPartyHosts: Set = [ - "https://first-party.com", // sanitize to → "first-party.com" - "http://api.first-party.com", // sanitize to → "api.first-party.com" - "https://first-party.com/v2/api", // sanitize to → "first-party.com" - "https://192.168.0.1/api", // sanitize to → "192.168.0.1" - "https://192.168.0.2", // sanitize to → "192.168.0.2" - "invalid-host-name", // drop - "192.168.0.3:8080", // drop - "", // drop - "localhost", // accept - "192.168.0.4", // accept - "valid-host-name.com", // accept + "https://first-party.com", + "http://api.first-party.com", + "https://first-party.com/v2/api" ] // Then - let configuration = try FeaturesConfiguration( + let mockHostsSanitizer = MockHostsSanitizer() + _ = try FeaturesConfiguration( configuration: .mockWith(rumEnabled: true, firstPartyHosts: firstPartyHosts), - appContext: .mockAny() - ) - - XCTAssertEqual( - configuration.urlSessionAutoInstrumentation?.userDefinedFirstPartyHosts, - [ - "first-party.com", - "api.first-party.com", - "localhost", - "192.168.0.1", - "192.168.0.2", - "localhost", - "192.168.0.4", - "valid-host-name.com" - ] + appContext: .mockAny(), + hostsSanitizer: mockHostsSanitizer ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: '192.168.0.3:8080' is not a valid host name and will be dropped.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: '' is not a valid host name and will be dropped.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: 'https://first-party.com' is an url and will be sanitized to: 'first-party.com'.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: 'https://192.168.0.1/api' is an url and will be sanitized to: '192.168.0.1'.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: 'http://api.first-party.com' is an url and will be sanitized to: 'api.first-party.com'.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: 'https://first-party.com/v2/api' is an url and will be sanitized to: 'first-party.com'.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: 'invalid-host-name' is not a valid host name and will be dropped.") - ) - XCTAssertTrue( - printFunction.printedMessages.contains("⚠️ The first party host configured for Datadog SDK is not valid: 'https://192.168.0.2' is an url and will be sanitized to: '192.168.0.2'.") - ) - XCTAssertEqual(printFunction.printedMessages.count, 8) + XCTAssertEqual(mockHostsSanitizer.sanitizations.count, 1) + let sanitization = try XCTUnwrap(mockHostsSanitizer.sanitizations.first) + XCTAssertEqual(sanitization.hosts, firstPartyHosts) + XCTAssertEqual(sanitization.warningMessage, "The first party host configured for Datadog SDK is not valid") } // MARK: - Helpers diff --git a/Tests/DatadogTests/Datadog/Core/Utils/HostsSanitizerTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/HostsSanitizerTests.swift new file mode 100644 index 0000000000..e04dcd5267 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Utils/HostsSanitizerTests.swift @@ -0,0 +1,75 @@ +/* + * 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 HostsSanitizerTests: XCTestCase { + func testSanitizationAndWarningMessages() throws { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { print($0) } } + + // When + let hosts: Set = [ + "https://first-party.com", // sanitize to → "first-party.com" + "http://api.first-party.com", // sanitize to → "api.first-party.com" + "https://first-party.com/v2/api", // sanitize to → "first-party.com" + "https://192.168.0.1/api", // sanitize to → "192.168.0.1" + "https://192.168.0.2", // sanitize to → "192.168.0.2" + "invalid-host-name", // drop + "192.168.0.3:8080", // drop + "", // drop + "localhost", // accept + "192.168.0.4", // accept + "valid-host-name.com", // accept + ] + + // Then + let sanitizer = HostsSanitizer() + let sanitizedHosts = sanitizer.sanitized(hosts: hosts, warningMessage: "Host is not valid") + + XCTAssertEqual( + sanitizedHosts, + [ + "first-party.com", + "api.first-party.com", + "localhost", + "192.168.0.1", + "192.168.0.2", + "localhost", + "192.168.0.4", + "valid-host-name.com" + ] + ) + + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: '192.168.0.3:8080' is not a valid host name and will be dropped.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: '' is not a valid host name and will be dropped.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://first-party.com' is an url and will be sanitized to: 'first-party.com'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://192.168.0.1/api' is an url and will be sanitized to: '192.168.0.1'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'http://api.first-party.com' is an url and will be sanitized to: 'api.first-party.com'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://first-party.com/v2/api' is an url and will be sanitized to: 'first-party.com'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'invalid-host-name' is not a valid host name and will be dropped.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://192.168.0.2' is an url and will be sanitized to: '192.168.0.2'.") + ) + XCTAssertEqual(printFunction.printedMessages.count, 8) + } +} diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index 27d3eee8d6..1934070769 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -1118,6 +1118,14 @@ extension DDError: RandomMockable { } } +class MockHostsSanitizer: HostsSanitizing { + private(set) var sanitizations = [(hosts: Set, warningMessage: String)]() + func sanitized(hosts: Set, warningMessage: String) -> Set { + sanitizations.append((hosts: hosts, warningMessage: warningMessage)) + return hosts + } +} + // MARK: - Global Dependencies Mocks /// Mock which can be used to intercept messages printed by `developerLogger` or diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift index 2fbbe91e01..134d1c5067 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift @@ -55,10 +55,12 @@ extension RUMViewEvent: RandomMockable { static func mockRandom() -> RUMViewEvent { return RUMViewEvent( dd: .init( + browserSdkVersion: nil, documentVersion: .mockRandom(), session: .init(plan: .plan1) ), application: .init(id: .mockRandom()), + ciTest: nil, connectivity: .mockRandom(), context: .mockRandom(), date: .mockRandom(), @@ -68,6 +70,7 @@ extension RUMViewEvent: RandomMockable { id: .mockRandom(), type: .user ), + source: .ios, synthetics: nil, usr: .mockRandom(), view: .init( @@ -117,12 +120,14 @@ extension RUMResourceEvent: RandomMockable { static func mockRandom() -> RUMResourceEvent { return RUMResourceEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1), spanId: .mockRandom(), traceId: .mockRandom() ), action: .init(id: .mockRandom()), application: .init(id: .mockRandom()), + ciTest: nil, connectivity: .mockRandom(), context: .mockRandom(), date: .mockRandom(), @@ -152,6 +157,7 @@ extension RUMResourceEvent: RandomMockable { id: .mockRandom(), type: .user ), + source: .ios, synthetics: nil, usr: .mockRandom(), view: .init( @@ -167,6 +173,7 @@ extension RUMActionEvent: RandomMockable { static func mockRandom() -> RUMActionEvent { return RUMActionEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: .init( @@ -180,6 +187,7 @@ extension RUMActionEvent: RandomMockable { type: [.tap, .swipe, .scroll].randomElement()! ), application: .init(id: .mockRandom()), + ciTest: nil, connectivity: .mockRandom(), context: .mockRandom(), date: .mockRandom(), @@ -189,6 +197,7 @@ extension RUMActionEvent: RandomMockable { id: .mockRandom(), type: .user ), + source: .ios, synthetics: nil, usr: .mockRandom(), view: .init( @@ -211,10 +220,12 @@ extension RUMErrorEvent: RandomMockable { static func mockRandom() -> RUMErrorEvent { return RUMErrorEvent( dd: .init( + browserSdkVersion: nil, session: .init(plan: .plan1) ), action: .init(id: .mockRandom()), application: .init(id: .mockRandom()), + ciTest: nil, connectivity: .mockRandom(), context: .mockRandom(), date: .mockRandom(), @@ -245,6 +256,7 @@ extension RUMErrorEvent: RandomMockable { id: .mockRandom(), type: .user ), + source: .ios, synthetics: nil, usr: .mockRandom(), view: .init( @@ -260,15 +272,20 @@ extension RUMErrorEvent: RandomMockable { extension RUMLongTaskEvent: RandomMockable { static func mockRandom() -> RUMLongTaskEvent { return RUMLongTaskEvent( - dd: .init(session: .init(plan: .plan1)), + dd: .init( + browserSdkVersion: nil, + session: .init(plan: .plan1) + ), action: .init(id: .mockRandom()), application: .init(id: .mockRandom()), + ciTest: nil, connectivity: .mockRandom(), context: .mockRandom(), date: .mockRandom(), longTask: .init(duration: .mockRandom(), id: .mockRandom(), isFrozenFrame: .mockRandom()), service: .mockRandom(), session: .init(hasReplay: false, id: .mockRandom(), type: .user), + source: .ios, synthetics: nil, usr: .mockRandom(), view: .init(id: .mockRandom(), name: .mockRandom(), referrer: .mockRandom(), url: .mockRandom()) diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 664ff958c0..75259448f7 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -630,6 +630,7 @@ extension RUMScopeDependencies { networkConnectionInfoProvider: NetworkConnectionInfoProviderMock(networkConnectionInfo: nil), carrierInfoProvider: CarrierInfoProviderMock(carrierInfo: nil) ), + serviceName: String = .mockAny(), eventBuilder: RUMEventBuilder = RUMEventBuilder(eventsMapper: .mockNoOp()), eventOutput: RUMEventOutput = RUMEventOutputMock(), rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), @@ -642,6 +643,7 @@ extension RUMScopeDependencies { userInfoProvider: userInfoProvider, launchTimeProvider: launchTimeProvider, connectivityInfoProvider: connectivityInfoProvider, + serviceName: serviceName, eventBuilder: eventBuilder, eventOutput: eventOutput, rumUUIDGenerator: rumUUIDGenerator, @@ -660,6 +662,7 @@ extension RUMScopeDependencies { userInfoProvider: RUMUserInfoProvider? = nil, launchTimeProvider: LaunchTimeProviderType? = nil, connectivityInfoProvider: RUMConnectivityInfoProvider? = nil, + serviceName: String? = nil, eventBuilder: RUMEventBuilder? = nil, eventOutput: RUMEventOutput? = nil, rumUUIDGenerator: RUMUUIDGenerator? = nil, @@ -672,6 +675,7 @@ extension RUMScopeDependencies { userInfoProvider: userInfoProvider ?? self.userInfoProvider, launchTimeProvider: launchTimeProvider ?? self.launchTimeProvider, connectivityInfoProvider: connectivityInfoProvider ?? self.connectivityInfoProvider, + serviceName: serviceName ?? self.serviceName, eventBuilder: eventBuilder ?? self.eventBuilder, eventOutput: eventOutput ?? self.eventOutput, rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScopeTests.swift index 5a66e0e32c..fabe884c92 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScopeTests.swift @@ -9,7 +9,11 @@ import XCTest class RUMResourceScopeTests: XCTestCase { private let output = RUMEventOutputMock() - private lazy var dependencies: RUMScopeDependencies = .mockWith(eventOutput: output) + private let randomServiceName: String = .mockRandom() + private lazy var dependencies: RUMScopeDependencies = .mockWith( + serviceName: randomServiceName, + eventOutput: output + ) private let context = RUMContext.mockWith( rumApplicationID: "rum-123", sessionID: .mockRandom(), @@ -76,6 +80,7 @@ class RUMResourceScopeTests: XCTestCase { XCTAssertEqual(event.model.application.id, scope.context.rumApplicationID) XCTAssertEqual(event.model.session.id, scope.context.sessionID.toRUMDataFormat) XCTAssertEqual(event.model.session.type, .user) + XCTAssertEqual(event.model.source, .ios) XCTAssertEqual(event.model.view.id, context.activeViewID?.toRUMDataFormat) XCTAssertEqual(event.model.view.url, "FooViewController") XCTAssertEqual(event.model.view.name, "FooViewName") @@ -98,6 +103,7 @@ class RUMResourceScopeTests: XCTestCase { XCTAssertEqual(event.model.dd.traceId, "100") XCTAssertEqual(event.model.dd.spanId, "200") XCTAssertEqual(event.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertEqual(event.model.service, randomServiceName) } func testGivenStartedResource_whenResourceLoadingEnds_itSendsResourceEventWithCustomSpanAndTraceId() throws { @@ -161,6 +167,8 @@ class RUMResourceScopeTests: XCTestCase { XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar"]) XCTAssertEqual(event.model.dd.traceId, "100") XCTAssertEqual(event.model.dd.spanId, "200") + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testGivenStartedResource_whenResourceLoadingEndsWithError_itSendsErrorEvent() throws { @@ -215,6 +223,8 @@ class RUMResourceScopeTests: XCTestCase { XCTAssertEqual(try XCTUnwrap(event.model.action?.id), context.activeUserActionID?.toRUMDataFormat) XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar"]) XCTAssertEqual(event.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testGivenStartedResource_whenResourceReceivesMetricsBeforeItEnds_itUsesMetricValuesInSentResourceEvent() throws { @@ -323,6 +333,8 @@ class RUMResourceScopeTests: XCTestCase { XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar"]) XCTAssertNil(event.model.dd.traceId) XCTAssertNil(event.model.dd.spanId) + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testGivenMultipleResourceScopes_whenSendingResourceEvents_eachEventHasUniqueResourceID() throws { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 3781bd8172..88b9af541c 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -444,31 +444,43 @@ class RUMSessionScopeTests: XCTestCase { // MARK: - Usage - func testWhenNoActiveViewScopes_itLogsWarning() { - // Given - let scope: RUMSessionScope = .mockWith(parent: parent, startTime: Date()) - XCTAssertEqual(scope.viewScopes.count, 0) - - let previousUserLogger = userLogger - defer { userLogger = previousUserLogger } - - let logOutput = LogOutputMock() - userLogger = .mockWith(logOutput: logOutput) - - let command = RUMCommandMock(time: Date(), canStartBackgroundView: false) - - // When - _ = scope.process(command: command) - - // Then - XCTAssertEqual(scope.viewScopes.count, 0) + func testGivenSessionWithNoActiveScope_whenReceivingRUMCommandOtherThanKeepSessionAliveCommand_itLogsWarning() throws { + func recordLogOnReceiving(command: RUMCommand) -> LogEvent? { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + XCTAssertEqual(scope.viewScopes.count, 0) + + let previousUserLogger = userLogger + defer { userLogger = previousUserLogger } + + let logOutput = LogOutputMock() + userLogger = .mockWith(logOutput: logOutput) + + // When + _ = scope.process(command: command) + + // Then + XCTAssertEqual(scope.viewScopes.count, 0) + return logOutput.recordedLog + } + + let randomCommand = RUMCommandMock(time: Date(), canStartBackgroundView: false, canStartApplicationLaunchView: false) + let randomCommandLog = try XCTUnwrap(recordLogOnReceiving(command: randomCommand)) + XCTAssertEqual(randomCommandLog.status, .warn) XCTAssertEqual( - logOutput.recordedLog?.message, + randomCommandLog.message, """ - \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the + \(String(describing: randomCommand)) was detected, but no view is active. To track views automatically, try calling the DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using the RumMonitor.startView() and RumMonitor.stopView() methods. """ ) + + let keepAliveCommand = RUMKeepSessionAliveCommand(time: Date(), attributes: [:]) + let keepAliveLog = recordLogOnReceiving(command: keepAliveCommand) + XCTAssertNil(keepAliveLog, "It shouldn't log warning when receiving `RUMKeepSessionAliveCommand`") } } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScopeTests.swift index 0a6dc208f2..73243e81ff 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScopeTests.swift @@ -9,7 +9,11 @@ import XCTest class RUMUserActionScopeTests: XCTestCase { private let output = RUMEventOutputMock() - private lazy var dependencies: RUMScopeDependencies = .mockWith(eventOutput: output) + private let randomServiceName: String = .mockRandom() + private lazy var dependencies: RUMScopeDependencies = .mockWith( + serviceName: randomServiceName, + eventOutput: output + ) private let parent = RUMContextProviderMock( context: .mockWith( rumApplicationID: "rum-123", @@ -58,6 +62,8 @@ class RUMUserActionScopeTests: XCTestCase { let recordedAction = try XCTUnwrap(recordedActionEvents.last) XCTAssertEqual(recordedAction.model.action.type.rawValue, String(describing: mockUserActionCmd.actionType)) XCTAssertEqual(recordedAction.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertEqual(recordedAction.model.source, .ios) + XCTAssertEqual(recordedAction.model.service, randomServiceName) } // MARK: - Continuous User Action @@ -102,6 +108,8 @@ class RUMUserActionScopeTests: XCTestCase { XCTAssertEqual(event.model.action.resource?.count, 0) XCTAssertEqual(event.model.action.error?.count, 0) XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar"]) + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testWhenContinuousUserActionExpires_itSendsActionEvent() throws { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index ef8441aba7..db347a1f6b 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -11,7 +11,11 @@ import UIKit class RUMViewScopeTests: XCTestCase { private let output = RUMEventOutputMock() private let parent = RUMContextProviderMock() - private lazy var dependencies: RUMScopeDependencies = .mockWith(eventOutput: output) + private let randomServiceName: String = .mockRandom() + private lazy var dependencies: RUMScopeDependencies = .mockWith( + serviceName: randomServiceName, + eventOutput: output + ) func testDefaultContext() { let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") @@ -66,9 +70,8 @@ class RUMViewScopeTests: XCTestCase { let scope = RUMViewScope( isInitialView: true, parent: parent, - dependencies: .mockWith( - launchTimeProvider: LaunchTimeProviderMock(launchTime: 2), // 2 seconds - eventOutput: output + dependencies: dependencies.replacing( + launchTimeProvider: LaunchTimeProviderMock(launchTime: 2) // 2 seconds ), identity: mockView, path: "UIViewController", @@ -92,6 +95,8 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.action.type, .applicationStart) XCTAssertEqual(event.model.action.loadingTime, 2_000_000_000) // 2e+9 ns XCTAssertEqual(event.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testWhenInitialViewReceivesAnyCommand_itSendsViewUpdateEvent() throws { @@ -126,6 +131,8 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.view.resource.count, 0) XCTAssertEqual(event.model.dd.documentVersion, 1) XCTAssertEqual(event.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testWhenViewIsStarted_itSendsViewUpdateEvent() throws { @@ -165,6 +172,8 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.view.resource.count, 0) XCTAssertEqual(event.model.dd.documentVersion, 1) XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar 2", "fizz": "buzz"]) + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testWhenViewIsStopped_itSendsViewUpdateEvent_andEndsTheScope() throws { @@ -217,6 +226,8 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.view.resource.count, 0) XCTAssertEqual(event.model.dd.documentVersion, 2) XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar"]) + XCTAssertEqual(event.model.source, .ios) + XCTAssertEqual(event.model.service, randomServiceName) } func testWhenAnotherViewIsStarted_itEndsTheScope() throws { @@ -586,6 +597,8 @@ class RUMViewScopeTests: XCTestCase { let firstActionEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).first) XCTAssertEqual(lastViewEvent.model.view.action.count, 1, "View should record 1 only custom action (pending action is not yet finished)") XCTAssertEqual(firstActionEvent.model.action.target?.name, customActionName) + XCTAssertEqual(firstActionEvent.model.source, .ios) + XCTAssertEqual(firstActionEvent.model.service, randomServiceName) } func testGivenViewWithNoPendingAction_whenCustomActionIsAdded_itSendsItInstantly() throws { @@ -619,6 +632,8 @@ class RUMViewScopeTests: XCTestCase { let firstActionEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).first) XCTAssertEqual(lastViewEvent.model.view.action.count, 1, "View should record custom action") XCTAssertEqual(firstActionEvent.model.action.target?.name, customActionName) + XCTAssertEqual(firstActionEvent.model.source, .ios) + XCTAssertEqual(firstActionEvent.model.service, randomServiceName) } // MARK: - Error Tracking @@ -671,6 +686,8 @@ class RUMViewScopeTests: XCTestCase { XCTAssertNil(error.model.action) XCTAssertEqual(error.model.context?.contextInfo as? [String: String], ["foo": "bar"]) XCTAssertEqual(error.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertEqual(error.model.source, .ios) + XCTAssertEqual(error.model.service, randomServiceName) let viewUpdate = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) XCTAssertEqual(viewUpdate.model.view.error.count, 1) @@ -699,12 +716,15 @@ class RUMViewScopeTests: XCTestCase { ) let error = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) - XCTAssertEqual(error.model.error.sourceType, .reactNative) XCTAssertTrue(error.model.error.isCrash ?? false) + XCTAssertEqual(error.model.source, .ios) + XCTAssertEqual(error.model.service, randomServiceName) let viewUpdate = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) XCTAssertEqual(viewUpdate.model.view.error.count, 1) + XCTAssertEqual(viewUpdate.model.source, .ios) + XCTAssertEqual(viewUpdate.model.service, randomServiceName) } func testWhenResourceIsFinishedWithError_itSendsViewUpdateEvent() throws { @@ -786,10 +806,12 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(longTask.context?.contextInfo as? [String: String], ["foo": "bar"]) XCTAssertEqual(longTask.date, longTaskStartingDate.timeIntervalSince1970.toInt64Milliseconds) XCTAssertEqual(longTask.dd.session?.plan, .plan1) + XCTAssertEqual(longTask.source, .ios) XCTAssertEqual(longTask.longTask.duration, (1.0).toInt64Nanoseconds) XCTAssertTrue(longTask.longTask.isFrozenFrame == true) XCTAssertEqual(longTask.view.id, scope.viewUUID.toRUMDataFormat) XCTAssertNil(longTask.synthetics) + XCTAssertEqual(longTask.service, randomServiceName) let viewUpdate = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) XCTAssertEqual(viewUpdate.model.view.longTask?.count, 1) diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift new file mode 100644 index 0000000000..b134ec73f6 --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift @@ -0,0 +1,149 @@ +/* + * 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 WebKit +@testable import Datadog + +final class DDUserContentController: WKUserContentController { + typealias NameHandlerPair = (name: String, handler: WKScriptMessageHandler) + private(set) var messageHandlers = [NameHandlerPair]() + + override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { + messageHandlers.append((name: name, handler: scriptMessageHandler)) + } +} + +final class MockScriptMessage: WKScriptMessage { + let mockBody: Any + + init(body: Any) { + self.mockBody = body + } + + override var body: Any { return mockBody } +} + +class WKUserContentController_DatadogTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(RUMFeature.instance) + temporaryFeatureDirectories.create() + } + + override func tearDown() { + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(RUMFeature.instance) + temporaryFeatureDirectories.delete() + super.tearDown() + } + + func testItAddsUserScriptAndMessageHandler() throws { + let mockSanitizer = MockHostsSanitizer() + let controller = DDUserContentController() + + let initialUserScriptCount = controller.userScripts.count + + controller.addDatadogMessageHandler(allowedWebViewHosts: ["datadoghq.com"], hostsSanitizer: mockSanitizer) + + XCTAssertEqual(controller.userScripts.count, initialUserScriptCount + 1) + XCTAssertEqual(controller.messageHandlers.map({ $0.name }), ["DatadogEventBridge"]) + + XCTAssertEqual(mockSanitizer.sanitizations.count, 1) + let sanitization = try XCTUnwrap(mockSanitizer.sanitizations.first) + XCTAssertEqual(sanitization.hosts, ["datadoghq.com"]) + XCTAssertEqual(sanitization.warningMessage, "The allowed WebView host configured for Datadog SDK is not valid") + } + + func testItLogsInvalidWebMessages() throws { + let previousUserLogger = userLogger + defer { userLogger = previousUserLogger } + let output = LogOutputMock() + userLogger = .mockWith(logOutput: output) + + let controller = DDUserContentController() + controller.addDatadogMessageHandler(allowedWebViewHosts: ["datadoghq.com"], hostsSanitizer: MockHostsSanitizer()) + + let messageHandler = try XCTUnwrap(controller.messageHandlers.first?.handler) as? DatadogMessageHandler + // non-string body is passed + messageHandler?.userContentController(controller, didReceive: MockScriptMessage(body: 123)) + messageHandler?.queue.sync { } + + XCTAssertEqual(output.recordedLog?.status, .error) + let userLogMessage = try XCTUnwrap(output.recordedLog?.message) + XCTAssertEqual(userLogMessage, #"🔥 Web Event Error: invalidMessage(description: "123")"#) + } + + func testSendingWebEvents() throws { + let dateProvider = RelativeDateProvider(startingFrom: Date(), advancingBySeconds: 1) + LoggingFeature.instance = .mockByRecordingLogMatchers( + directories: temporaryFeatureDirectories, + configuration: .mockWith( + common: .mockWith( + applicationVersion: "1.0.0", + applicationBundleIdentifier: "com.datadoghq.ios-sdk", + serviceName: "default-service-name", + environment: "tests", + sdkVersion: "1.2.3" + ) + ), + dependencies: .mockWith( + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) + ) + ) + RUMFeature.instance = .mockByRecordingRUMEventMatchers( + directories: temporaryFeatureDirectories, + dependencies: .mockWith( + dateProvider: dateProvider + ) + ) + Global.rum = RUMMonitor.initialize() + defer { + LoggingFeature.instance?.deinitialize() + Global.rum = DDNoopRUMMonitor() + RUMFeature.instance?.deinitialize() + } + + let controller = DDUserContentController() + controller.addDatadogMessageHandler( + allowedWebViewHosts: ["datadoghq.com"], + hostsSanitizer: MockHostsSanitizer() + ) + + let messageHandler = try XCTUnwrap(controller.messageHandlers.first?.handler) as? DatadogMessageHandler + let webLogMessage = MockScriptMessage(body: #"{"eventType":"log","event":{"date":1635932927012,"error":{"origin":"console"},"message":"console error: error","session_id":"0110cab4-7471-480e-aa4e-7ce039ced355","status":"error","view":{"referrer":"","url":"https://datadoghq.dev/browser-sdk-test-playground"}},"tags":["browser_sdk_version:3.6.13"]}"#) + messageHandler?.userContentController(controller, didReceive: webLogMessage) + + let logMatcher = try LoggingFeature.waitAndReturnLogMatchers(count: 1)[0] + + logMatcher.assertValue(forKey: "date", equals: 1_635_932_927_012) + logMatcher.assertValue(forKey: "ddtags", equals: "version:1.0.0,env:tests") + logMatcher.assertValue(forKey: "message", equals: "console error: error") + logMatcher.assertValue(forKey: "status", equals: "error") + logMatcher.assertValue( + forKey: "view", + equals: ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ) + logMatcher.assertValue( + forKey: "error", + equals: ["origin": "console"] + ) + // 00000000-0000-0000-0000-000000000000 is session_id of mock RUM context + logMatcher.assertValue(forKey: "session_id", equals: "00000000-0000-0000-0000-000000000000") + + let webRUMMessage = MockScriptMessage(body: #"{"eventType":"view","event":{"application":{"id":"xxx"},"date":1635933113708,"service":"super","session":{"id":"0110cab4-7471-480e-aa4e-7ce039ced355","type":"user"},"type":"view","view":{"action":{"count":0},"cumulative_layout_shift":0,"dom_complete":152800000,"dom_content_loaded":118300000,"dom_interactive":116400000,"error":{"count":0},"first_contentful_paint":121300000,"id":"64308fd4-83f9-48cb-b3e1-1e91f6721230","in_foreground_periods":[],"is_active":true,"largest_contentful_paint":121299000,"load_event":152800000,"loading_time":152800000,"loading_type":"initial_load","long_task":{"count":0},"referrer":"","resource":{"count":3},"time_spent":3120000000,"url":"http://localhost:8080/test.html"},"_dd":{"document_version":2,"drift":0,"format_version":2,"session":{"plan":2}}},"tags":["browser_sdk_version:3.6.13"]}"#) + messageHandler?.userContentController(controller, didReceive: webRUMMessage) + + let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 1) + try rumEventMatchers[0].model(ofType: RUMViewEvent.self) { rumModel in + XCTAssertEqual(rumModel.application.id, "abc") + XCTAssertEqual(rumModel.view.id, "64308fd4-83f9-48cb-b3e1-1e91f6721230") + } + } +} diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift new file mode 100644 index 0000000000..757f96db3f --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift @@ -0,0 +1,99 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +fileprivate class MockEventConsumer: WebLogEventConsumer, WebRUMEventConsumer { + private(set) var consumedLogEvents: [JSON] = [] + private(set) var consumedInternalLogEvents: [JSON] = [] + private(set) var consumedRUMEvents: [JSON] = [] + + func consume(event: JSON, internalLog: Bool) throws { + if internalLog { + consumedInternalLogEvents.append(event) + } else { + consumedLogEvents.append(event) + } + } + + func consume(event: JSON) throws { + consumedRUMEvents.append(event) + } +} + +class WebEventBridgeTests: XCTestCase { + private let mockLogEventConsumer = MockEventConsumer() + private let mockRUMEventConsumer = MockEventConsumer() + lazy var eventBridge = WebEventBridge( + logEventConsumer: mockLogEventConsumer, + rumEventConsumer: mockRUMEventConsumer + ) + + // MARK: - Parsing + + func testWhenMessageIsInvalid_itFailsParsing() { + let messageInvalidJSON = """ + { 123: foobar } + """ + XCTAssertThrowsError( + try eventBridge.consume(messageInvalidJSON), + "Non-string keys (123) should throw" + ) + } + + // MARK: - Routing + + func testWhenEventTypeIsMissing_itThrows() { + let messageMissingEventType = """ + {"event":{"date":1635932927012,"error":{"origin":"console"}}} + """ + XCTAssertThrowsError( + try eventBridge.consume(messageMissingEventType), + "Missing eventType should throw" + ) { error in + XCTAssertEqual( + error as? WebEventError, + WebEventError.missingKey(key: WebEventBridge.Constants.eventTypeKey) + ) + } + } + + func testWhenEventTypeIsLog_itGoesToLogEventConsumer() throws { + let messageLog = """ + {"eventType":"log","event":{"date":1635932927012,"error":{"origin":"console"},"message":"console error: error","session_id":"0110cab4-7471-480e-aa4e-7ce039ced355","status":"error","view":{"referrer":"","url":"https://datadoghq.dev/browser-sdk-test-playground"}},"tags":["browser_sdk_version:3.6.13"]} + """ + try eventBridge.consume(messageLog) + + XCTAssertEqual(mockLogEventConsumer.consumedLogEvents.count, 1) + XCTAssertEqual(mockLogEventConsumer.consumedInternalLogEvents.count, 0) + XCTAssertEqual(mockLogEventConsumer.consumedRUMEvents.count, 0) + XCTAssertEqual(mockRUMEventConsumer.consumedLogEvents.count, 0) + XCTAssertEqual(mockRUMEventConsumer.consumedInternalLogEvents.count, 0) + XCTAssertEqual(mockRUMEventConsumer.consumedRUMEvents.count, 0) + + let consumedEvent = try XCTUnwrap(mockLogEventConsumer.consumedLogEvents.first) + XCTAssertEqual(consumedEvent["session_id"] as? String, "0110cab4-7471-480e-aa4e-7ce039ced355") + XCTAssertEqual((consumedEvent["view"] as? JSON)?["url"] as? String, "https://datadoghq.dev/browser-sdk-test-playground") + } + + func testWhenEventTypeIsNonLog_itGoesToRUMEventConsumer() throws { + let messageRUM = """ + {"eventType":"view","event":{"application":{"id":"xxx"},"date":1635933113708,"service":"super","session":{"id":"0110cab4-7471-480e-aa4e-7ce039ced355","type":"user"},"type":"view","view":{"action":{"count":0},"cumulative_layout_shift":0,"dom_complete":152800000,"dom_content_loaded":118300000,"dom_interactive":116400000,"error":{"count":0},"first_contentful_paint":121300000,"id":"64308fd4-83f9-48cb-b3e1-1e91f6721230","in_foreground_periods":[],"is_active":true,"largest_contentful_paint":121299000,"load_event":152800000,"loading_time":152800000,"loading_type":"initial_load","long_task":{"count":0},"referrer":"","resource":{"count":3},"time_spent":3120000000,"url":"http://localhost:8080/test.html"},"_dd":{"document_version":2,"drift":0,"format_version":2,"session":{"plan":2}}},"tags":["browser_sdk_version:3.6.13"]} + """ + try eventBridge.consume(messageRUM) + + XCTAssertEqual( + mockLogEventConsumer.consumedLogEvents.count + mockLogEventConsumer.consumedInternalLogEvents.count, + 0 + ) + XCTAssertEqual(mockRUMEventConsumer.consumedRUMEvents.count, 1) + + let consumedEvent = try XCTUnwrap(mockRUMEventConsumer.consumedRUMEvents.first) + XCTAssertEqual((consumedEvent["session"] as? JSON)?["id"] as? String, "0110cab4-7471-480e-aa4e-7ce039ced355") + XCTAssertEqual((consumedEvent["view"] as? JSON)?["url"] as? String, "http://localhost:8080/test.html") + } +} diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WebLogEventConsumerTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WebLogEventConsumerTests.swift new file mode 100644 index 0000000000..9915d2237a --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WebLogEventConsumerTests.swift @@ -0,0 +1,136 @@ +/* + * 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 WebLogEventConsumerTests: XCTestCase { + let mockUserLogsWriter = FileWriterMock() + let mockInternalLogsWriter = FileWriterMock() + let mockDateCorrector = DateCorrectorMock() + let mockContextProvider = RUMContextProviderMock(context: .mockWith(rumApplicationID: "123456")) + + func testWhenValidWebLogEventPassed_itDecoratesAndPassesToWriter() throws { + let mockSessionID: UUID = .mockRandom() + mockContextProvider.context.sessionID = RUMUUID(rawValue: mockSessionID) + mockDateCorrector.correctionOffset = 123 + let applicationVersion = String.mockRandom() + let environment = String.mockRandom() + let eventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: mockUserLogsWriter, + internalLogsWriter: mockInternalLogsWriter, + dateCorrector: mockDateCorrector, + rumContextProvider: mockContextProvider, + applicationVersion: applicationVersion, + environment: environment + ) + + let webLogEvent: JSON = [ + "date": 1_635_932_927_012, + "error": ["origin": "console"], + "message": "console error: error", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "error", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + let expectedWebLogEvent: JSON = [ + "date": 1_635_932_927_012 + 123.toInt64Milliseconds, + "error": ["origin": "console"], + "message": "console error: error", + "application_id": "123456", + "session_id": mockSessionID.uuidString.lowercased(), + "status": "error", + "ddtags": "version:\(applicationVersion),env:\(environment)", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + + try eventConsumer.consume(event: webLogEvent, internalLog: false) + + let data = try JSONEncoder().encode(mockUserLogsWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebLogEvent) + + XCTAssertNil(mockInternalLogsWriter.dataWritten) + } + + func testWhenValidWebInternalLogEventPassed_itDecoratesAndPassesToWriter() throws { + let mockSessionID: UUID = .mockRandom() + mockContextProvider.context.sessionID = RUMUUID(rawValue: mockSessionID) + mockDateCorrector.correctionOffset = 123 + let applicationVersion = String.mockRandom() + let environment = String.mockRandom() + let eventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: mockUserLogsWriter, + internalLogsWriter: mockInternalLogsWriter, + dateCorrector: mockDateCorrector, + rumContextProvider: mockContextProvider, + applicationVersion: applicationVersion, + environment: environment + ) + + let webLogEvent: JSON = [ + "date": 1_635_932_927_012, + "error": ["origin": "console"], + "message": "console error: error", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "error", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + let expectedWebLogEvent: JSON = [ + "date": 1_635_932_927_012 + 123.toInt64Milliseconds, + "error": ["origin": "console"], + "message": "console error: error", + "application_id": "123456", + "session_id": mockSessionID.uuidString.lowercased(), + "status": "error", + "ddtags": "version:\(applicationVersion),env:\(environment)", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + + try eventConsumer.consume(event: webLogEvent, internalLog: true) + + let data = try JSONEncoder().encode(mockInternalLogsWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebLogEvent) + + XCTAssertNil(mockUserLogsWriter.dataWritten) + } + + func testWhenContextIsUnavailable_itPassesEventAsIs() throws { + let applicationVersion = String.mockRandom() + let environment = String.mockRandom() + let eventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: mockUserLogsWriter, + internalLogsWriter: mockInternalLogsWriter, + dateCorrector: mockDateCorrector, + rumContextProvider: nil, + applicationVersion: applicationVersion, + environment: environment + ) + + let webLogEvent: JSON = [ + "date": 1_635_932_927_012, + "error": ["origin": "console"], + "message": "console error: error", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "error", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + var expectedWebLogEvent: JSON = webLogEvent + expectedWebLogEvent["ddtags"] = "version:\(applicationVersion),env:\(environment)" + + try eventConsumer.consume(event: webLogEvent, internalLog: false) + + let data = try JSONEncoder().encode(mockUserLogsWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebLogEvent) + + XCTAssertNil(mockInternalLogsWriter.dataWritten) + } +} diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WebRUMEventConsumerTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WebRUMEventConsumerTests.swift new file mode 100644 index 0000000000..7493490ac2 --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WebRUMEventConsumerTests.swift @@ -0,0 +1,147 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class WebRUMEventConsumerTests: XCTestCase { + let mockWriter = FileWriterMock() + let mockDateCorrector = DateCorrectorMock() + let mockContextProvider = RUMContextProviderMock(context: .mockWith(rumApplicationID: "123456")) + let mockCommandSubscriber = RUMCommandSubscriberMock() + let mockDateProvider = RelativeDateProvider(startingFrom: .mockDecember15th2019At10AMUTC(), advancingBySeconds: 0.0) + + func testWhenValidWebRUMEventPassed_itDecoratesAndPassesToWriter() throws { + let mockSessionID = UUID(uuidString: "e9796469-c2a1-43d6-b0f6-65c47d33cf5f")! + mockContextProvider.context.sessionID = RUMUUID(rawValue: mockSessionID) + mockDateCorrector.correctionOffset = 123 + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: mockContextProvider, + rumCommandSubscriber: mockCommandSubscriber, + dateProvider: mockDateProvider + ) + + let webRUMEvent: JSON = [ + "_dd": [ + "session": ["plan": 2] + ], + "application": ["id": "75d50c62-8b66-403c-a453-aaa1c44d64bd"], + "date": 1_640_252_823_292, + "service": "shopist-web-ui", + "session": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "view": [ + "id": "00413060-599f-4a77-80de-5d3beab3da2e" + ], + "type": "action" + ] + let expectedWebRUMEvent: JSON = [ + "_dd": [ + "session": ["plan": 1] + ], + "application": ["id": mockContextProvider.context.rumApplicationID], + "date": 1_640_252_823_292 + 123.toInt64Milliseconds, + "service": "shopist-web-ui", + "session": ["id": mockContextProvider.context.sessionID.toRUMDataFormat], + "view": [ + "id": "00413060-599f-4a77-80de-5d3beab3da2e" + ], + "type": "action" + ] + + try eventConsumer.consume(event: webRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebRUMEvent) + let webViewCommand = try XCTUnwrap(mockCommandSubscriber.lastReceivedCommand) + XCTAssertEqual(webViewCommand.time, .mockDecember15th2019At10AMUTC()) + } + + func testWhenValidWebRUMEventPassedWithoutRUMContext_itPassesToWriter() throws { + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: nil, + rumCommandSubscriber: mockCommandSubscriber, + dateProvider: mockDateProvider + ) + + let webRUMEvent: JSON = [ + "_dd": [ + "session": ["plan": 2] + ], + "application": ["id": "75d50c62-8b66-403c-a453-aaa1c44d64bd"], + "date": 1_640_252_823_292, + "service": "shopist-web-ui", + "session": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "view": [ + "id": "00413060-599f-4a77-80de-5d3beab3da2e" + ], + "type": "action" + ] + + try eventConsumer.consume(event: webRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, webRUMEvent) + let webViewCommand = try XCTUnwrap(mockCommandSubscriber.lastReceivedCommand) + XCTAssertEqual(webViewCommand.time, .mockDecember15th2019At10AMUTC()) + } + + func testWhenNativeSessionIsSampledOut_itPassesWebEventToWriter() throws { + mockContextProvider.context.sessionID = RUMUUID.nullUUID + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: mockContextProvider, + rumCommandSubscriber: mockCommandSubscriber, + dateProvider: mockDateProvider + ) + + let webRUMEvent: JSON = [ + "new_key": "new_value", + "type": "unknown" + ] + + try eventConsumer.consume(event: webRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, webRUMEvent) + let webViewCommand = try XCTUnwrap(mockCommandSubscriber.lastReceivedCommand) + XCTAssertEqual(webViewCommand.time, .mockDecember15th2019At10AMUTC()) + } + + func testWhenUnknownWebRUMEventPassed_itPassesToWriter() throws { + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: mockContextProvider, + rumCommandSubscriber: mockCommandSubscriber, + dateProvider: mockDateProvider + ) + + let unknownWebRUMEvent: JSON = [ + "new_key": "new_value", + "type": "unknown" + ] + + try eventConsumer.consume(event: unknownWebRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, unknownWebRUMEvent) + let webViewCommand = try XCTUnwrap(mockCommandSubscriber.lastReceivedCommand) + XCTAssertEqual(webViewCommand.time, .mockDecember15th2019At10AMUTC()) + } +} diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index babb66296c..df350b738e 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -27,8 +27,12 @@ class RUMMonitorTests: XCTestCase { func testStartingViewIdentifiedByViewController() throws { let dateProvider = RelativeDateProvider(startingFrom: Date(), advancingBySeconds: 1) + let randomServiceName: String = .mockRandom() RUMFeature.instance = .mockByRecordingRUMEventMatchers( directories: temporaryFeatureDirectories, + configuration: .mockWith( + common: .mockWith(serviceName: randomServiceName) + ), dependencies: .mockWith( dateProvider: dateProvider ) @@ -46,16 +50,20 @@ class RUMMonitorTests: XCTestCase { verifyGlobalAttributes(in: rumEventMatchers) try rumEventMatchers[0].model(ofType: RUMActionEvent.self) { rumModel in XCTAssertEqual(rumModel.action.type, .applicationStart) + XCTAssertEqual(rumModel.service, randomServiceName) } try rumEventMatchers[1].model(ofType: RUMViewEvent.self) { rumModel in XCTAssertEqual(rumModel.view.action.count, 1) + XCTAssertEqual(rumModel.service, randomServiceName) } try rumEventMatchers[2].model(ofType: RUMViewEvent.self) { rumModel in XCTAssertEqual(rumModel.view.action.count, 1) XCTAssertEqual(rumModel.view.timeSpent, 1_000_000_000) + XCTAssertEqual(rumModel.service, randomServiceName) } try rumEventMatchers[3].model(ofType: RUMViewEvent.self) { rumModel in XCTAssertEqual(rumModel.view.action.count, 0) + XCTAssertEqual(rumModel.service, randomServiceName) } } diff --git a/Tests/DatadogTests/Matchers/RUMEventMatcher.swift b/Tests/DatadogTests/Matchers/RUMEventMatcher.swift index 0c0db8b5ff..391512b9c5 100644 --- a/Tests/DatadogTests/Matchers/RUMEventMatcher.swift +++ b/Tests/DatadogTests/Matchers/RUMEventMatcher.swift @@ -27,11 +27,15 @@ internal class RUMEventMatcher { /// ``` /// /// **See Also** `RUMEventMatcher.fromJSONObjectData(_:)` - /// - class func fromNewlineSeparatedJSONObjectsData(_ data: Data) throws -> [RUMEventMatcher] { + /// - Parameter data: payload data + /// - Parameter eventsPatch: optional transformation to apply on each event within the payload before instantiating matcher (default: `nil`) + class func fromNewlineSeparatedJSONObjectsData(_ data: Data, eventsPatch: ((Data) throws -> Data)? = nil) throws -> [RUMEventMatcher] { let separator = "\n".data(using: .utf8)![0] - let spansData = data.split(separator: separator).map { Data($0) } - return try spansData.map { spanJSONData in try RUMEventMatcher.fromJSONObjectData(spanJSONData) } + var eventsData = data.split(separator: separator).map { Data($0) } + if let patch = eventsPatch { + eventsData = try eventsData.map { try patch($0) } + } + return try eventsData.map { eventJSONData in try RUMEventMatcher.fromJSONObjectData(eventJSONData) } } let jsonData: Data diff --git a/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift b/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift index 0870331a1e..e269859845 100644 --- a/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift +++ b/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift @@ -48,7 +48,8 @@ internal class RUMSessionMatcher { /// The `name` of the visited RUM View. /// Corresponds to the "VIEW NAME" in RUM Explorer. - fileprivate(set) var name: String = "" + /// Might be `nil` for events received from Browser SDK. + fileprivate(set) var name: String? /// The `path` of the visited RUM View. /// Corresponds to the "VIEW URL" in RUM Explorer. @@ -136,8 +137,8 @@ internal class RUMSessionMatcher { if let visit = visitsByViewID[rumEvent.view.id] { visit.viewEvents.append(rumEvent) visit.viewEventMatchers.append(matcher) - if visit.name.isEmpty { - visit.name = rumEvent.view.name! + if visit.name == nil { + visit.name = rumEvent.view.name } else if visit.name != rumEvent.view.name { throw RUMSessionConsistencyException( description: "The RUM View name: \(rumEvent) is different than other RUM View names for the same `view.id`." @@ -324,7 +325,7 @@ extension RUMSessionMatcher: CustomStringConvertible { return " → [⛔️ Invalid View - it has no view events]" } - var description = " → [📸 View (name: '\(viewVisit.name)', id: \(viewVisit.viewID), duration: \(seconds(from: lastViewEvent.view.timeSpent)) actions.count: \(lastViewEvent.view.action.count), resources.count: \(lastViewEvent.view.resource.count), errors.count: \(lastViewEvent.view.error.count), longTask.count: \(lastViewEvent.view.longTask?.count ?? 0), frozenFrames.count: \(lastViewEvent.view.frozenFrame?.count ?? 0)]" + var description = " → [📸 View (name: '\(viewVisit.name ?? "nil")', id: \(viewVisit.viewID), duration: \(seconds(from: lastViewEvent.view.timeSpent)) actions.count: \(lastViewEvent.view.action.count), resources.count: \(lastViewEvent.view.resource.count), errors.count: \(lastViewEvent.view.error.count), longTask.count: \(lastViewEvent.view.longTask?.count ?? 0), frozenFrames.count: \(lastViewEvent.view.frozenFrame?.count ?? 0)]" if !viewVisit.actionEvents.isEmpty { description += "\n → action events:"