diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000000..f59b5814df --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - platform: ios + documentation_targets: ["Datadog", "DatadogObjc", "DatadogCrashReporting"] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2457070207..e677304748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Unreleased -# 1.21.0 / 27-07-2023 +# 1.22.0 / 21-07-2023 +- [BUGFIX] Fix APM local spans not correlating with RUM views. See [#1355][] +- [IMPROVEMENT] Reduce number of view updates by filtering events from payload. See [#1328][] + +# 1.21.0 / 27-06-2023 - [BUGFIX] Fix TracingUUID string format. See [#1311][] (Thanks [@changm4n][]) - [BUGFIX] Rename _Datadog_Private to DatadogPrivate. See [#1331] (Thanks [@alexfanatics][]) - [IMPROVEMENT] Add context to crash when there's an active view. See [#1315][] + # 1.20.0 / 01-06-2023 - [BUGFIX] Use targetTimestamp as reference to calculate FPS for variable refresh rate displays. See [#1272][] @@ -472,6 +477,8 @@ [#1311]: https://github.com/DataDog/dd-sdk-ios/pull/1311 [#1315]: https://github.com/DataDog/dd-sdk-ios/pull/1315 [#1331]: https://github.com/DataDog/dd-sdk-ios/pull/1331 +[#1328]: https://github.com/DataDog/dd-sdk-ios/pull/1328 +[#1355]: https://github.com/DataDog/dd-sdk-ios/pull/1355 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index dd3616490f..da6ecf6ad0 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 3C0839F12A431E930040A213 /* DataFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0839F02A431E930040A213 /* DataFormatTests.swift */; }; + 3C0839F22A431E9E0040A213 /* DataFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0839F02A431E930040A213 /* DataFormatTests.swift */; }; + 3C6953532A45C02D00542049 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6953522A45C02D00542049 /* Event.swift */; }; + 3C6953542A45C02D00542049 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6953522A45C02D00542049 /* Event.swift */; }; + 3C9A376A2A4595EF00414CD6 /* EventGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9A37692A4595EF00414CD6 /* EventGeneratorTests.swift */; }; + 3C9A376B2A4595EF00414CD6 /* EventGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9A37692A4595EF00414CD6 /* EventGeneratorTests.swift */; }; + 3CB992952A434EE100B6C6CF /* RUMViewEventsFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB992942A434EE100B6C6CF /* RUMViewEventsFilterTests.swift */; }; + 3CB992962A434EE100B6C6CF /* RUMViewEventsFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB992942A434EE100B6C6CF /* RUMViewEventsFilterTests.swift */; }; 490D5EC929C9E17E004F969C /* RUMStopSessionScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */; }; 490D5ECF29CA0745004F969C /* RUMStopSessionScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */; }; 490D5ED029CA074A004F969C /* KioskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ECD29CA0738004F969C /* KioskViewController.swift */; }; @@ -157,8 +165,6 @@ 6141015B251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141015A251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift */; }; 61410167251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */; }; 61411B1024EC15AC0012EAB2 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; - 6141CE672806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141CE662806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift */; }; - 6141CE682806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141CE662806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift */; }; 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */; }; 61441C0C24616DE9003D8BB8 /* Main iOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0A24616DE9003D8BB8 /* Main iOS.storyboard */; }; 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; @@ -180,8 +186,6 @@ 61441C9D2461A796003D8BB8 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C9C2461A796003D8BB8 /* AppConfiguration.swift */; }; 6147E3B3270486920092BC9F /* TracingConfigurationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6147E3B2270486920092BC9F /* TracingConfigurationE2ETests.swift */; }; 614872772485067300E3EBDB /* SpanTagsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614872762485067300E3EBDB /* SpanTagsReducer.swift */; }; - 61494B7A27F352570082BBCC /* RUMViewUpdatesThrottler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494B7927F352570082BBCC /* RUMViewUpdatesThrottler.swift */; }; - 61494B7B27F352570082BBCC /* RUMViewUpdatesThrottler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494B7927F352570082BBCC /* RUMViewUpdatesThrottler.swift */; }; 61494CB124C839460082C633 /* RUMResourceScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB024C839460082C633 /* RUMResourceScope.swift */; }; 61494CB524C864680082C633 /* RUMResourceScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */; }; 61494CBA24CB126F0082C633 /* RUMUserActionScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */; }; @@ -1367,6 +1371,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3C0839F02A431E930040A213 /* DataFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFormatTests.swift; sourceTree = ""; }; + 3C6953522A45C02D00542049 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 3C9A37692A4595EF00414CD6 /* EventGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGeneratorTests.swift; sourceTree = ""; }; + 3CB992942A434EE100B6C6CF /* RUMViewEventsFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewEventsFilterTests.swift; sourceTree = ""; }; 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMStopSessionScenarioTests.swift; sourceTree = ""; }; 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMStopSessionScenario.storyboard; sourceTree = ""; }; 490D5ECD29CA0738004F969C /* KioskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskViewController.swift; sourceTree = ""; }; @@ -1516,7 +1524,6 @@ 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzlerTests.swift; sourceTree = ""; }; 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Casting+RUM.swift"; sourceTree = ""; }; 61417DC52525CDDE00E2D55C /* TaskInterception.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskInterception.swift; sourceTree = ""; }; - 6141CE662806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewUpdatesThrottlerTests.swift; sourceTree = ""; }; 61441C0224616DE9003D8BB8 /* Example iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppDelegate.swift; sourceTree = ""; }; 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Main iOS.storyboard"; sourceTree = ""; }; @@ -1537,7 +1544,6 @@ 61441C9C2461A796003D8BB8 /* AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; }; 6147E3B2270486920092BC9F /* TracingConfigurationE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationE2ETests.swift; sourceTree = ""; }; 614872762485067300E3EBDB /* SpanTagsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanTagsReducer.swift; sourceTree = ""; }; - 61494B7927F352570082BBCC /* RUMViewUpdatesThrottler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewUpdatesThrottler.swift; sourceTree = ""; }; 61494CB024C839460082C633 /* RUMResourceScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMResourceScope.swift; sourceTree = ""; }; 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMResourceScopeTests.swift; sourceTree = ""; }; 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUserActionScope.swift; sourceTree = ""; }; @@ -2216,6 +2222,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C0839EF2A431E7D0040A213 /* Upload */ = { + isa = PBXGroup; + children = ( + 3C0839F02A431E930040A213 /* DataFormatTests.swift */, + ); + path = Upload; + sourceTree = ""; + }; 490D5ECA29C9E28F004F969C /* StopSessionScenario */ = { isa = PBXGroup; children = ( @@ -2643,6 +2657,7 @@ 619E16D42577C11B00B2516B /* Writing */, 619E16D52577C12100B2516B /* Reading */, 61133C2B2423990D00786299 /* Files */, + 3C9A37692A4595EF00414CD6 /* EventGeneratorTests.swift */, ); path = Persistence; sourceTree = ""; @@ -3072,7 +3087,6 @@ children = ( 61C1510C25AC8C1B00362D4B /* RUMViewIdentityTests.swift */, 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */, - 6141CE662806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift */, ); path = Utils; sourceTree = ""; @@ -3169,7 +3183,6 @@ children = ( 61FF9A4425AC5DEA001058CC /* RUMViewIdentity.swift */, 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */, - 61494B7927F352570082BBCC /* RUMViewUpdatesThrottler.swift */, ); path = Utils; sourceTree = ""; @@ -3853,6 +3866,7 @@ D236BE2A29521A7700676E67 /* Integrations */, 61786F7524FCDDE2009E6BAB /* Debugging */, 61411B0E24EC15940012EAB2 /* Utils */, + 3CB992942A434EE100B6C6CF /* RUMViewEventsFilterTests.swift */, ); path = RUM; sourceTree = ""; @@ -4419,6 +4433,7 @@ 61133BB42423979B00786299 /* URLRequestBuilder.swift */, 61AD4E3724531500006E34EA /* DataFormat.swift */, D24C27E9270C8BEE005DE596 /* DataCompression.swift */, + 3C6953522A45C02D00542049 /* Event.swift */, ); path = Upload; sourceTree = ""; @@ -4464,6 +4479,7 @@ D2956CAE2869D516007D5462 /* DatadogInternal */ = { isa = PBXGroup; children = ( + 3C0839EF2A431E7D0040A213 /* Upload */, A736BA3629D1B7AC00C00966 /* Extensions */, D2CBC25C294215BE00134409 /* Codable */, D2956CAF2869D520007D5462 /* Context */, @@ -5590,7 +5606,6 @@ 614B0A4F24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift in Sources */, D2CBC2572942008800134409 /* AnyDecodable.swift in Sources */, 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */, - 61494B7A27F352570082BBCC /* RUMViewUpdatesThrottler.swift in Sources */, D2B3F04A2829510600C2B5EE /* DatadogCoreProtocol.swift in Sources */, 6122514827FDFF82004F5AE4 /* RUMScopeDependencies.swift in Sources */, 615950EE291C058F00470E0C /* SessionReplayDependency.swift in Sources */, @@ -5743,6 +5758,7 @@ 61D3E0D7277B23F1008BE766 /* KronosData+Bytes.swift in Sources */, 61E5332C24B75C51003D6C4E /* RUMFeature.swift in Sources */, 6156CB9D24E18600008CB2B2 /* TracingWithRUMIntegration.swift in Sources */, + 3C6953532A45C02D00542049 /* Event.swift in Sources */, 61C5A88624509A0C00DA608C /* TracingUUIDGenerator.swift in Sources */, 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */, 9EAF0CF8275A2FDC0044E8CA /* HostsSanitizer.swift in Sources */, @@ -5776,6 +5792,7 @@ D2A1EE3B287EECC000D28DFB /* CarrierInfoPublisherTests.swift in Sources */, 61410167251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift in Sources */, 61FF282824B8A31E000B3D9B /* RUMEventMatcher.swift in Sources */, + 3C9A376A2A4595EF00414CD6 /* EventGeneratorTests.swift in Sources */, D29294E3291D652C00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */, 61C2C20924C0C75500C0321C /* RUMSessionScopeTests.swift in Sources */, 61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */, @@ -5845,6 +5862,7 @@ 61B03879252724AB00518F3C /* URLSessionInterceptorTests.swift in Sources */, 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */, 6184751526EFCF1300C7C9C5 /* DatadogTestsObserver.swift in Sources */, + 3C0839F12A431E930040A213 /* DataFormatTests.swift in Sources */, 61133C602423990D00786299 /* RequestBuilderTests.swift in Sources */, 61133C572423990D00786299 /* FileReaderTests.swift in Sources */, D2A1EE38287EEB7400D28DFB /* NetworkConnectionInfoPublisherTests.swift in Sources */, @@ -5874,6 +5892,7 @@ D2956CB12869D54E007D5462 /* DeviceInfoTests.swift in Sources */, 61B558D42469CDD8001460D3 /* TracingUUIDGeneratorTests.swift in Sources */, 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */, + 3CB992952A434EE100B6C6CF /* RUMViewEventsFilterTests.swift in Sources */, 613F23E4252B062F006CD2D7 /* TaskInterceptionTests.swift in Sources */, 61FB2230244E1BE900902D19 /* LoggingFeatureTests.swift in Sources */, 61E5333124B75DFC003D6C4E /* RUMFeatureMocks.swift in Sources */, @@ -5940,7 +5959,6 @@ 614B0A4D24EBD71500A2A780 /* RUMUserInfoProviderTests.swift in Sources */, 61DA8CB828647A500074A606 /* InternalLoggerTests.swift in Sources */, 9EF963E82537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift in Sources */, - 6141CE672806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift in Sources */, 61F3CDAB25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift in Sources */, A728ADA32934DB5000397996 /* W3CHTTPHeadersWriterTests.swift in Sources */, 9E989A4225F640D100235FC3 /* AppStateListenerTests.swift in Sources */, @@ -6270,7 +6288,6 @@ 6122514927FDFF82004F5AE4 /* RUMScopeDependencies.swift in Sources */, D2CB6E2B27C50EAE00A62B57 /* RUMViewScope.swift in Sources */, D2CB6E2C27C50EAE00A62B57 /* KronosNTPPacket.swift in Sources */, - 61494B7B27F352570082BBCC /* RUMViewUpdatesThrottler.swift in Sources */, D2CB6E2F27C50EAE00A62B57 /* SpanTagsReducer.swift in Sources */, D2CB6E3027C50EAE00A62B57 /* RUMApplicationScope.swift in Sources */, D2D37DC02846335F00FB4348 /* DatadogV1CoreProtocol.swift in Sources */, @@ -6460,6 +6477,7 @@ D2CB6EC027C50EAE00A62B57 /* RUMFeature.swift in Sources */, D2CB6EC127C50EAE00A62B57 /* TracingWithRUMIntegration.swift in Sources */, D2CB6EC327C50EAE00A62B57 /* TracingUUIDGenerator.swift in Sources */, + 3C6953542A45C02D00542049 /* Event.swift in Sources */, D2CB6EC427C50EAE00A62B57 /* DataUploadDelay.swift in Sources */, D2CB6EC527C50EAE00A62B57 /* HostsSanitizer.swift in Sources */, 61E945E42869BF3D00A946C4 /* CoreLogger.swift in Sources */, @@ -6519,6 +6537,7 @@ D2CB6EF527C520D400A62B57 /* TracerConfigurationTests.swift in Sources */, D2CB6EF627C520D400A62B57 /* DDSpanTests.swift in Sources */, D2CB6EF727C520D400A62B57 /* URLSessionAutoInstrumentationMocks.swift in Sources */, + 3CB992962A434EE100B6C6CF /* RUMViewEventsFilterTests.swift in Sources */, D2CB6EF927C520D400A62B57 /* WebViewEventReceiverTests.swift in Sources */, D2CB6EFA27C520D400A62B57 /* DirectoriesMock.swift in Sources */, D21C26EF28AFB65B005DD405 /* ErrorMessageReceiverTests.swift in Sources */, @@ -6554,6 +6573,7 @@ D2CB6F1427C520D400A62B57 /* UUID.swift in Sources */, D2CB6F1527C520D400A62B57 /* URLSessionRUMResourcesHandlerTests.swift in Sources */, A762BDE529351A250058D8E7 /* FirstPartyHostsTests.swift in Sources */, + 3C0839F22A431E9E0040A213 /* DataFormatTests.swift in Sources */, D2CB6F1627C520D400A62B57 /* URLSessionInterceptorTests.swift in Sources */, D2CB6F1727C520D400A62B57 /* ObjcExceptionHandlerTests.swift in Sources */, D2CB6F1827C520D400A62B57 /* DatadogTestsObserver.swift in Sources */, @@ -6652,13 +6672,13 @@ D2CB6F7127C520D400A62B57 /* URLSessionTracingHandlerTests.swift in Sources */, D2CB6F7227C520D400A62B57 /* ValuePublisherTests.swift in Sources */, D2CB6F7327C520D400A62B57 /* CoreTelephonyMocks.swift in Sources */, + 3C9A376B2A4595EF00414CD6 /* EventGeneratorTests.swift in Sources */, D20605B7287572640047275C /* DatadogContextProviderMock.swift in Sources */, D2CB6F7427C520D400A62B57 /* VitalCPUReaderTests.swift in Sources */, D2CB6F7527C520D400A62B57 /* UIKitExtensionsTests.swift in Sources */, D2CB6F7627C520D400A62B57 /* RUMUserInfoProviderTests.swift in Sources */, 61DA8CB928647A500074A606 /* InternalLoggerTests.swift in Sources */, D2CB6F7727C520D400A62B57 /* DDURLSessionDelegateAsSuperclassTests.swift in Sources */, - 6141CE682806B41C00EBB879 /* RUMViewUpdatesThrottlerTests.swift in Sources */, D2CB6F7927C520D400A62B57 /* UIViewControllerSwizzlerTests.swift in Sources */, D2CB6F7A27C520D400A62B57 /* AppStateListenerTests.swift in Sources */, D2CB6F7B27C520D400A62B57 /* RUMApplicationScopeTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme index 997f0a6f16..858f895f27 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme @@ -32,8 +32,8 @@ Void) { @@ -138,40 +187,80 @@ internal struct DebugRUMSessionView: View { var body: some View { VStack() { - HStack { - FormItemView( - title: "RUM View", placeholder: "view key", accent: .rumViewColor, value: $viewModel.viewKey - ) - Button("START") { viewModel.startView() } - } - HStack { - FormItemView( - title: "RUM Action", placeholder: "name", accent: .rumActionColor, value: $viewModel.actionName - ) - Button("ADD") { viewModel.addAction() } - } - HStack { - FormItemView( - title: "RUM Error", placeholder: "message", accent: .rumErrorColor, value: $viewModel.errorMessage - ) - Button("ADD") { viewModel.addError() } + Group { + Text("RUM Session") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.bold)) + Text("Debug RUM Session by creating events manually:") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.light)) + HStack { + FormItemView( + title: "RUM View", placeholder: "view key", accent: .rumViewColor, value: $viewModel.viewKey + ) + Button("START") { viewModel.startView() } + } + HStack { + FormItemView( + title: "RUM Action", placeholder: "name", accent: .rumActionColor, value: $viewModel.actionName + ) + Button("ADD") { viewModel.addAction() } + } + HStack { + FormItemView( + title: "RUM Error", placeholder: "message", accent: .rumErrorColor, value: $viewModel.errorMessage + ) + Button("ADD") { viewModel.addError() } + } + HStack { + FormItemView( + title: "RUM Resource", placeholder: "key", accent: .rumResourceColor, value: $viewModel.resourceKey + ) + Button("START") { viewModel.startResource() } + } + Divider() } - HStack { - FormItemView( - title: "RUM Resource", placeholder: "key", accent: .rumResourceColor, value: $viewModel.resourceKey - ) - Button("START") { viewModel.startResource() } + Group { + Text("Bundling Logs and Spans") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.bold)) + Text("Debug bundling Logs and Spans with RUM Session by sending them manually while the session is active.") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.light)) + HStack { + FormItemView( + title: "Log", placeholder: "log message", accent: .gray, value: $viewModel.logMessage + ) + Button("Send") { viewModel.sendLog() } + } + HStack { + FormItemView( + title: "Span", placeholder: "span name", accent: .gray, value: $viewModel.spanOperationName + ) + Button("Send") { viewModel.sendSpan() } + } + Text("Send 1st party request with instrumented URLSession:") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.light)) + HStack { + FormItemView( + title: "POST Request", placeholder: "request url", accent: .gray, value: $viewModel.instrumentedRequestURL + ) + Button("Send") { viewModel.sendPOSTRequest() } + } + Divider() } - Divider() - Text("RUM Session:") - .bold() - .font(.footnote) - List(viewModel.sessionItems) { sessionItem in - SessionItemView(item: sessionItem) - .listRowInsets(EdgeInsets()) - .padding(4) + Group { + Text("Current RUM Session:") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.bold)) + List(viewModel.sessionItems) { sessionItem in + SessionItemView(item: sessionItem) + .listRowInsets(EdgeInsets()) + .padding(4) + } + .listStyle(PlainListStyle()) } - .listStyle(PlainListStyle()) } .buttonStyle(DatadogButtonStyle()) .padding() diff --git a/DatadogSDK.podspec b/DatadogSDK.podspec index fcc10e8be3..5df70894f8 100644 --- a/DatadogSDK.podspec +++ b/DatadogSDK.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDK" s.module_name = "Datadog" - s.version = "1.21.0" + s.version = "1.22.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKAlamofireExtension.podspec b/DatadogSDKAlamofireExtension.podspec index 8d25eff348..d3f9c4eb5f 100644 --- a/DatadogSDKAlamofireExtension.podspec +++ b/DatadogSDKAlamofireExtension.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKAlamofireExtension" s.module_name = "DatadogAlamofireExtension" - s.version = "1.21.0" + s.version = "1.22.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKCrashReporting.podspec b/DatadogSDKCrashReporting.podspec index f9a6cbb644..30d7f7b0eb 100644 --- a/DatadogSDKCrashReporting.podspec +++ b/DatadogSDKCrashReporting.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKCrashReporting" s.module_name = "DatadogCrashReporting" - s.version = "1.21.0" + s.version = "1.22.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" @@ -22,6 +22,6 @@ Pod::Spec.new do |s| s.static_framework = true s.source_files = "Sources/DatadogCrashReporting/**/*.swift" - s.dependency 'DatadogSDK', '1.21.0' + s.dependency 'DatadogSDK', '1.22.0' s.dependency 'PLCrashReporter', '~> 1.11.0' end diff --git a/DatadogSDKObjc.podspec b/DatadogSDKObjc.podspec index ac120c7d02..7b23e6433b 100644 --- a/DatadogSDKObjc.podspec +++ b/DatadogSDKObjc.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKObjc" s.module_name = "DatadogObjc" - s.version = "1.21.0" + s.version = "1.22.0" s.summary = "Official Datadog Objective-C SDK for iOS." s.homepage = "https://www.datadoghq.com" @@ -21,5 +21,5 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/DataDog/dd-sdk-ios.git', :tag => s.version.to_s } s.source_files = "Sources/DatadogObjc/**/*.swift" - s.dependency 'DatadogSDK', '1.21.0' + s.dependency 'DatadogSDK', '1.22.0' end diff --git a/DatadogSDKSessionReplay.podspec b/DatadogSDKSessionReplay.podspec index 7021d79d9c..8d2550b0cd 100644 --- a/DatadogSDKSessionReplay.podspec +++ b/DatadogSDKSessionReplay.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKSessionReplay" s.module_name = "DatadogSessionReplay" - s.version = "1.21.0" + s.version = "1.22.0" s.summary = "Official Datadog Session Replay SDK for iOS. This module is currently in beta - contact Datadog to request a try." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard index 06a778a7b3..1c08a90aad 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard @@ -3,7 +3,7 @@ - + @@ -18,15 +18,15 @@ - - + + - - + + - + @@ -34,7 +34,7 @@ - + @@ -184,7 +184,7 @@ - + @@ -199,7 +199,7 @@ - + @@ -211,14 +211,14 @@ - + + + + + + + + + + @@ -247,17 +256,18 @@ - + - + - + + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/ImagesViewControllers.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/ImagesViewControllers.swift index 0db8e71456..5b714c4773 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/ImagesViewControllers.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/ImagesViewControllers.swift @@ -9,6 +9,7 @@ import UIKit internal class ImagesViewController: UIViewController { @IBOutlet weak var customButton: UIButton! @IBOutlet weak var customImageView: UIImageView! + @IBOutlet weak var contentImageView: UIImageView! @IBOutlet weak var tabBar: UITabBar! @IBOutlet weak var navigationBar: UINavigationBar! @@ -23,6 +24,8 @@ internal class ImagesViewController: UIViewController { navigationBar.setBackgroundImage(UIImage(color: color), for: .default) customImageView.image = UIImage(named: "dd_logo")?.withRenderingMode(.alwaysTemplate) + + contentImageView.image = UIImage(color: color) } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift index 8ed0358eb0..9ddbaba4a8 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift @@ -42,6 +42,8 @@ private extension SRWireframe { return text.toFrame() case .imageWireframe(value: let image): return image.toFrame() + case .placeholderWireframe(value: let placeholder): + return placeholder.toFrame() } } } @@ -85,6 +87,34 @@ private extension SRImageWireframe { } } +private extension SRPlaceholderWireframe { + func toFrame() -> BlueprintFrame { + BlueprintFrame( + x: CGFloat(x), + y: CGFloat(y), + width: CGFloat(width), + height: CGFloat(height), + style: frameStyle( + border: .init(color: "#000000FF", width: 4), + style: .init( + backgroundColor: "#A9A9A9FF", + cornerRadius: 0, + opacity: 1 + ) + ), + content: frameContent( + text: label ?? "Placeholder", + textStyle: .init(color: "#000000FF", family: "-apple-system", size: 24), + textPosition: .init( + alignment: .init(horizontal: .center, vertical: .center), + padding: .init(bottom: 0, left: 0, right: 0, top: 0) + ) + ) + ) + } +} + + private func frameStyle(border: SRShapeBorder?, style: SRShapeStyle?) -> BlueprintFrame.Style { var fs = BlueprintFrame.Style( lineWidth: 0, diff --git a/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMContextReceiver.swift b/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMContextReceiver.swift index 024ae5af93..0914075c76 100644 --- a/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMContextReceiver.swift +++ b/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMContextReceiver.swift @@ -11,8 +11,8 @@ import Datadog internal struct RUMContext: Decodable, Equatable { internal struct IDs: Decodable, Equatable { enum CodingKeys: String, CodingKey { - case applicationID = "application_id" - case sessionID = "session_id" + case applicationID = "application.id" + case sessionID = "session.id" case viewID = "view.id" } /// Current RUM application ID - standard UUID string, lowecased. diff --git a/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMDependency.swift b/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMDependency.swift index 0e1b256793..1353d40a8d 100644 --- a/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMDependency.swift +++ b/DatadogSessionReplay/Sources/DatadogCoreIntegration/RUMDependency.swift @@ -27,7 +27,7 @@ internal enum RUMDependency { /// /// SR expects: /// - `nil` if current RUM session is not sampled, - /// - baggage with `application_id`, `session_id` and `view.id` keys if RUM session is sampled. + /// - baggage with `application.id`, `session.id` and `view.id` keys if RUM session is sampled. static let ids = "ids" // MARK: Contract from SR to RUM (mirror of `SessionReplayDependency` in RUM): @@ -41,4 +41,7 @@ internal enum RUMDependency { /// The key referencing a `Bool` value that indicates if replay is being recorded. static let hasReplay = "has_replay" + + /// They key referencing a `[String: Int64]` dictionary of viewIDs and associated records count. + static let recordsCountByViewID = "records_count_by_view_id" } diff --git a/DatadogSessionReplay/Sources/DatadogCoreIntegration/RequestBuilder/RequestBuilder.swift b/DatadogSessionReplay/Sources/DatadogCoreIntegration/RequestBuilder/RequestBuilder.swift index 90dab1f3e5..e15459a1a4 100644 --- a/DatadogSessionReplay/Sources/DatadogCoreIntegration/RequestBuilder/RequestBuilder.swift +++ b/DatadogSessionReplay/Sources/DatadogCoreIntegration/RequestBuilder/RequestBuilder.swift @@ -13,13 +13,13 @@ internal struct RequestBuilder: FeatureRequestBuilder { /// Custom URL for uploading data to. let customUploadURL: URL? - func request(for events: [Data], with context: DatadogContext) throws -> URLRequest { + func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { let source = SRSegment.Source(rawValue: context.source) ?? .ios // TODO: RUMM-2410 Send telemetry on `?? .ios` let segmentBuilder = SegmentJSONBuilder(source: source) // If we can't decode `events: [Data]` there is no way to recover, so we throw an // error to let the core delete the batch: - let records = try events.map { try EnrichedRecordJSON(jsonObjectData: $0) } + let records = try events.map { try EnrichedRecordJSON(jsonObjectData: $0.data) } let segment = try segmentBuilder.createSegmentJSON(from: records) // If the SDK was configured with deprecated `set(*Endpoint:)` APIs we don't have `context.site`, so diff --git a/DatadogSessionReplay/Sources/DatadogCoreIntegration/SRContextPublisher.swift b/DatadogSessionReplay/Sources/DatadogCoreIntegration/SRContextPublisher.swift index 495031f43c..b521a1cbf9 100644 --- a/DatadogSessionReplay/Sources/DatadogCoreIntegration/SRContextPublisher.swift +++ b/DatadogSessionReplay/Sources/DatadogCoreIntegration/SRContextPublisher.swift @@ -15,11 +15,19 @@ internal class SRContextPublisher { self.core = core } - /// Notifies other Features on the state of Session Replay recording. - func setRecordingIsPending(_ value: Bool) { - core?.set( + /// Notifies other Features if Session Replay is recording. + func setHasReplay(_ value: Bool) { + core?.update( feature: RUMDependency.srBaggageKey, attributes: { [RUMDependency.hasReplay: value] } ) } + + /// Notifies other Features on the state of Session Replay records count. + func setRecordsCountByViewID(_ value: [String: Int64]) { + core?.update( + feature: RUMDependency.srBaggageKey, + attributes: { [RUMDependency.recordsCountByViewID: value] } + ) + } } diff --git a/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift index 20e7e1bfd2..418893f007 100644 --- a/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift @@ -37,7 +37,8 @@ internal class SessionReplayFeature: DatadogFeature, SessionReplayController { let processor = Processor( queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.processor"), - writer: writer + writer: writer, + srContextPublisher: SRContextPublisher(core: core) ) let scheduler = MainThreadScheduler(interval: 0.1) @@ -62,7 +63,9 @@ internal class SessionReplayFeature: DatadogFeature, SessionReplayController { self.requestBuilder = RequestBuilder(customUploadURL: configuration.customUploadURL) self.performanceOverride = PerformancePresetOverride( maxFileSize: UInt64(10).MB, - maxObjectSize: UInt64(10).MB + maxObjectSize: UInt64(10).MB, + meanFileAge: 5, // equivalent of `batchSize: .small` - see `DatadogCore.PerformancePreset` + minUploadDelay: 1 // equivalent of `uploadFrequency: .frequent` - see `DatadogCore.PerformancePreset` ) } diff --git a/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift b/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift index 7f9e2fa070..16ebc079c6 100644 --- a/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift +++ b/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift @@ -17,6 +17,8 @@ extension SRWireframe: Diffable { return wireframe.id case .textWireframe(let wireframe): return wireframe.id + case .placeholderWireframe(let wireframe): + return wireframe.id } } @@ -28,6 +30,8 @@ extension SRWireframe: Diffable { return this.hashValue != other.hashValue case let (.imageWireframe(this), .imageWireframe(other)): return this.hashValue != other.hashValue + case let (.placeholderWireframe(this), .placeholderWireframe(other)): + return this.hashValue != other.hashValue default: return true } @@ -64,6 +68,8 @@ extension SRWireframe: MutableWireframe { return try this.mutations(from: otherWireframe) case .textWireframe(let this): return try this.mutations(from: otherWireframe) + case .placeholderWireframe(let this): + return try this.mutations(from: otherWireframe) } } } @@ -92,6 +98,28 @@ extension SRShapeWireframe: MutableWireframe { } } +extension SRPlaceholderWireframe: MutableWireframe { + func mutations(from otherWireframe: SRWireframe) throws -> WireframeMutation { + guard case .placeholderWireframe(let other) = otherWireframe else { + throw WireframeMutationError.typeMismatch + } + guard other.id == id else { + throw WireframeMutationError.idMismatch + } + + return .placeholderWireframeUpdate( + value: .init( + clip: use(clip, ifDifferentThan: other.clip), + height: use(height, ifDifferentThan: other.height), + id: id, + width: use(width, ifDifferentThan: other.width), + x: use(x, ifDifferentThan: other.x), + y: use(y, ifDifferentThan: other.y) + ) + ) + } +} + extension SRImageWireframe: MutableWireframe { func mutations(from otherWireframe: SRWireframe) throws -> WireframeMutation { guard case .imageWireframe(let other) = otherWireframe else { @@ -108,6 +136,7 @@ extension SRImageWireframe: MutableWireframe { clip: use(clip, ifDifferentThan: other.clip), height: use(height, ifDifferentThan: other.height), id: id, + isEmpty: use(isEmpty, ifDifferentThan: other.isEmpty), mimeType: use(mimeType, ifDifferentThan: other.mimeType), shapeStyle: use(shapeStyle, ifDifferentThan: other.shapeStyle), width: use(width, ifDifferentThan: other.width), diff --git a/DatadogSessionReplay/Sources/Processor/Processor.swift b/DatadogSessionReplay/Sources/Processor/Processor.swift index acc8ca1b90..f3ccde323c 100644 --- a/DatadogSessionReplay/Sources/Processor/Processor.swift +++ b/DatadogSessionReplay/Sources/Processor/Processor.swift @@ -52,9 +52,18 @@ internal class Processor: Processing { var interceptWireframes: (([SRWireframe]) -> Void)? = nil #endif - init(queue: Queue, writer: Writing) { + private var srContextPublisher: SRContextPublisher + + private var recordsCountByViewID: [String: Int64] = [:] + + init( + queue: Queue, + writer: Writing, + srContextPublisher: SRContextPublisher + ) { self.queue = queue self.writer = writer + self.srContextPublisher = srContextPublisher } // MARK: - Processing @@ -114,6 +123,8 @@ internal class Processor: Processing { // Transform `[SRRecord]` to `EnrichedRecord` so we can write it to `DatadogCore` and // later read it back (as `EnrichedRecordJSON`) for preparing upload request(s): let enrichedRecord = EnrichedRecord(context: viewTreeSnapshot.context, records: records) + trackRecord(key: enrichedRecord.viewID, value: Int64(records.count)) + writer.write(nextRecord: enrichedRecord) } @@ -121,4 +132,13 @@ internal class Processor: Processing { lastSnapshot = viewTreeSnapshot lastWireframes = wireframes } + + private func trackRecord(key: String, value: Int64) { + if let existingValue = recordsCountByViewID[key] { + recordsCountByViewID[key] = existingValue + value + } else { + recordsCountByViewID[key] = value + } + srContextPublisher.setRecordsCountByViewID(recordsCountByViewID) + } } diff --git a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift index 43bc520729..ef917115b5 100644 --- a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift @@ -58,7 +58,7 @@ internal class WireframesBuilder { } func createImageWireframe( - base64: String?, + base64: String, id: WireframeID, frame: CGRect, mimeType: String = "png", @@ -75,7 +75,7 @@ internal class WireframesBuilder { clip: clip, height: Int64(withNoOverflow: frame.height), id: id, - isEmpty: base64?.isEmpty == true ? true : nil, + isEmpty: false, // field deprecated - we should use placeholder wireframe instead mimeType: mimeType, shapeStyle: createShapeStyle(backgroundColor: backgroundColor, cornerRadius: cornerRadius, opacity: opacity), width: Int64(withNoOverflow: frame.width), @@ -146,6 +146,24 @@ internal class WireframesBuilder { return .textWireframe(value: wireframe) } + func createPlaceholderWireframe( + id: Int64, + frame: CGRect, + label: String, + clip: SRContentClip? = nil + ) -> SRWireframe { + let wireframe = SRPlaceholderWireframe( + clip: clip, + height: Int64(withNoOverflow: frame.size.height), + id: id, + label: label, + width: Int64(withNoOverflow: frame.size.width), + x: Int64(withNoOverflow: frame.minX), + y: Int64(withNoOverflow: frame.minY) + ) + return .placeholderWireframe(value: wireframe) + } + // MARK: - Private private func createShapeBorder(borderColor: CGColor?, borderWidth: CGFloat?) -> SRShapeBorder? { diff --git a/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift b/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift index f0e51164cc..12a8978757 100644 --- a/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift +++ b/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift @@ -24,7 +24,7 @@ internal class RecordingCoordinator { sampler: Sampler ) { self.recorder = recorder - srContextPublisher.setRecordingIsPending(false) + srContextPublisher.setHasReplay(false) scheduler.schedule { [weak self] in guard let rumContext = self?.currentRUMContext, @@ -56,7 +56,7 @@ internal class RecordingCoordinator { scheduler.stop() } - srContextPublisher.setRecordingIsPending( + srContextPublisher.setHasReplay( self?.isSampled == true && self?.currentRUMContext?.ids.viewID != nil ) } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index fdca087905..0790756206 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -133,7 +133,7 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { opacity: attributes.alpha ) ] - var base64: String = "" + var base64: String? if shouldRecordImage { base64 = imageDataProvider.contentBase64String( of: image, @@ -141,14 +141,24 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { ) } if let contentFrame = contentFrame { - wireframes.append( - builder.createImageWireframe( - base64: base64, - id: imageWireframeID, - frame: contentFrame, - clip: clipsToBounds ? clip : nil + if let base64 = base64 { + wireframes.append( + builder.createImageWireframe( + base64: base64, + id: imageWireframeID, + frame: contentFrame, + clip: clipsToBounds ? clip : nil + ) ) - ) + } else { + wireframes.append( + builder.createPlaceholderWireframe( + id: imageWireframeID, + frame: clipsToBounds ? relativeIntersectedRect : contentFrame, + label: "Content Image" + ) + ) + } } return wireframes } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift index bb24bbbdfb..10990652d7 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift @@ -47,17 +47,10 @@ internal struct UnsupportedViewWireframesBuilder: NodeWireframesBuilder { func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { return [ - builder.createTextWireframe( + builder.createPlaceholderWireframe( id: wireframeID, frame: attributes.frame, - text: unsupportedClassName, - textFrame: attributes.frame, - textAlignment: .init(horizontal: .center, vertical: .center), - textColor: UIColor.red.cgColor, - borderColor: UIColor.lightGray.cgColor, - borderWidth: 1, - backgroundColor: UIColor(white: 0.95, alpha: 1).cgColor, - cornerRadius: 4 + label: unsupportedClassName ) ] } diff --git a/DatadogSessionReplay/Sources/Recorder/WindowObserver/KeyWindowObserver.swift b/DatadogSessionReplay/Sources/Recorder/WindowObserver/KeyWindowObserver.swift index c0eeaf7805..9ee438bf63 100644 --- a/DatadogSessionReplay/Sources/Recorder/WindowObserver/KeyWindowObserver.swift +++ b/DatadogSessionReplay/Sources/Recorder/WindowObserver/KeyWindowObserver.swift @@ -17,7 +17,6 @@ internal class KeyWindowObserver: AppWindowObserver { if #available(iOS 13.0, tvOS 13.0, *) { return findONiOS13AndLater() } else { - assertionFailure("TODO: RUMM-2409 `AppWindowObserver` isn't yet ready for this version of OS") return nil } } diff --git a/DatadogSessionReplay/Sources/Writer/Models/SRDataModels.swift b/DatadogSessionReplay/Sources/Writer/Models/SRDataModels.swift index ff0448ea3c..68b2625e76 100644 --- a/DatadogSessionReplay/Sources/Writer/Models/SRDataModels.swift +++ b/DatadogSessionReplay/Sources/Writer/Models/SRDataModels.swift @@ -374,11 +374,50 @@ internal struct SRImageWireframe: Codable, Hashable { } } +/// Schema of all properties of a PlaceholderWireframe. +internal struct SRPlaceholderWireframe: Codable, Hashable { + /// Schema of clipping information for a Wireframe. + internal let clip: SRContentClip? + + /// The height in pixels of the UI element, normalized based on the device pixels per inch density (DPI). Example: if a device has a DPI = 2, the height of all UI elements is divided by 2 to get a normalized height. + internal let height: Int64 + + /// Defines the unique ID of the wireframe. This is persistent throughout the view lifetime. + internal let id: Int64 + + /// Label of the placeholder + internal var label: String? + + /// The type of the wireframe. + internal let type: String = "placeholder" + + /// The width in pixels of the UI element, normalized based on the device pixels per inch density (DPI). Example: if a device has a DPI = 2, the width of all UI elements is divided by 2 to get a normalized width. + internal let width: Int64 + + /// The position in pixels on X axis of the UI element in absolute coordinates. The anchor point is always the top-left corner of the wireframe. + internal let x: Int64 + + /// The position in pixels on Y axis of the UI element in absolute coordinates. The anchor point is always the top-left corner of the wireframe. + internal let y: Int64 + + enum CodingKeys: String, CodingKey { + case clip = "clip" + case height = "height" + case id = "id" + case label = "label" + case type = "type" + case width = "width" + case x = "x" + case y = "y" + } +} + /// Schema of a Wireframe type. internal enum SRWireframe: Codable { case shapeWireframe(value: SRShapeWireframe) case textWireframe(value: SRTextWireframe) case imageWireframe(value: SRImageWireframe) + case placeholderWireframe(value: SRPlaceholderWireframe) // MARK: - Codable @@ -393,6 +432,8 @@ internal enum SRWireframe: Codable { try container.encode(value) case .imageWireframe(let value): try container.encode(value) + case .placeholderWireframe(let value): + try container.encode(value) } } @@ -412,6 +453,10 @@ internal enum SRWireframe: Codable { self = .imageWireframe(value: value) return } + if let value = try? container.decode(SRPlaceholderWireframe.self) { + self = .placeholderWireframe(value: value) + return + } let error = DecodingError.Context( codingPath: container.codingPath, debugDescription: """ @@ -569,6 +614,7 @@ internal struct SRIncrementalSnapshotRecord: Codable { case textWireframeUpdate(value: TextWireframeUpdate) case shapeWireframeUpdate(value: ShapeWireframeUpdate) case imageWireframeUpdate(value: ImageWireframeUpdate) + case placeholderWireframeUpdate(value: PlaceholderWireframeUpdate) // MARK: - Codable @@ -583,6 +629,8 @@ internal struct SRIncrementalSnapshotRecord: Codable { try container.encode(value) case .imageWireframeUpdate(let value): try container.encode(value) + case .placeholderWireframeUpdate(let value): + try container.encode(value) } } @@ -602,6 +650,10 @@ internal struct SRIncrementalSnapshotRecord: Codable { self = .imageWireframeUpdate(value: value) return } + if let value = try? container.decode(PlaceholderWireframeUpdate.self) { + self = .placeholderWireframeUpdate(value: value) + return + } let error = DecodingError.Context( codingPath: container.codingPath, debugDescription: """ @@ -761,6 +813,44 @@ internal struct SRIncrementalSnapshotRecord: Codable { case y = "y" } } + + /// Schema of all properties of a PlaceholderWireframe. + internal struct PlaceholderWireframeUpdate: Codable { + /// Schema of clipping information for a Wireframe. + internal let clip: SRContentClip? + + /// The height in pixels of the UI element, normalized based on the device pixels per inch density (DPI). Example: if a device has a DPI = 2, the height of all UI elements is divided by 2 to get a normalized height. + internal let height: Int64? + + /// Defines the unique ID of the wireframe. This is persistent throughout the view lifetime. + internal let id: Int64 + + /// Label of the placeholder + internal var label: String? + + /// The type of the wireframe. + internal let type: String = "placeholder" + + /// The width in pixels of the UI element, normalized based on the device pixels per inch density (DPI). Example: if a device has a DPI = 2, the width of all UI elements is divided by 2 to get a normalized width. + internal let width: Int64? + + /// The position in pixels on X axis of the UI element in absolute coordinates. The anchor point is always the top-left corner of the wireframe. + internal let x: Int64? + + /// The position in pixels on Y axis of the UI element in absolute coordinates. The anchor point is always the top-left corner of the wireframe. + internal let y: Int64? + + enum CodingKeys: String, CodingKey { + case clip = "clip" + case height = "height" + case id = "id" + case label = "label" + case type = "type" + case width = "width" + case x = "x" + case y = "y" + } + } } } @@ -1052,4 +1142,4 @@ internal enum SRRecord: Codable { } } -// Generated from https://github.com/DataDog/rum-events-format/tree/067bca66899474c390afe43377e4c31155290cf2 +// Generated from https://github.com/DataDog/rum-events-format/tree/e3d941c30622ff8624051604584ebd3f9fff2b25 diff --git a/DatadogSessionReplay/Tests/DatadogCoreIntegration/RUMContextReceiverTests.swift b/DatadogSessionReplay/Tests/DatadogCoreIntegration/RUMContextReceiverTests.swift index c74a81f41f..2489253fae 100644 --- a/DatadogSessionReplay/Tests/DatadogCoreIntegration/RUMContextReceiverTests.swift +++ b/DatadogSessionReplay/Tests/DatadogCoreIntegration/RUMContextReceiverTests.swift @@ -17,9 +17,9 @@ class RUMContextReceiverTests: XCTestCase { let context = DatadogContext.mockWith(featuresAttributes: [ RUMDependency.rumBaggageKey: [ RUMDependency.ids: [ - RUMDependency.IDs.applicationIDKey: "app-id", - RUMDependency.IDs.sessionIDKey: "session-id", - RUMDependency.IDs.viewIDKey: "view-id" + RUMContext.IDs.CodingKeys.applicationID.rawValue: "app-id", + RUMContext.IDs.CodingKeys.sessionID.rawValue: "session-id", + RUMContext.IDs.CodingKeys.viewID.rawValue: "view-id" ], RUMDependency.serverTimeOffsetKey: TimeInterval(123) ] @@ -68,9 +68,9 @@ class RUMContextReceiverTests: XCTestCase { let context1 = DatadogContext.mockWith(featuresAttributes: [ RUMDependency.rumBaggageKey: [ RUMDependency.ids: [ - RUMDependency.IDs.applicationIDKey: "app-id-1", - RUMDependency.IDs.sessionIDKey: "session-id-1", - RUMDependency.IDs.viewIDKey: "view-id-1" + RUMContext.IDs.CodingKeys.applicationID.rawValue: "app-id-1", + RUMContext.IDs.CodingKeys.sessionID.rawValue: "session-id-1", + RUMContext.IDs.CodingKeys.viewID.rawValue: "view-id-1" ], RUMDependency.serverTimeOffsetKey: TimeInterval(123) ] @@ -79,9 +79,9 @@ class RUMContextReceiverTests: XCTestCase { let context2 = DatadogContext.mockWith(featuresAttributes: [ RUMDependency.rumBaggageKey: [ RUMDependency.ids: [ - RUMDependency.IDs.applicationIDKey: "app-id-2", - RUMDependency.IDs.sessionIDKey: "session-id-2", - RUMDependency.IDs.viewIDKey: "view-id-2" + RUMContext.IDs.CodingKeys.applicationID.rawValue: "app-id-2", + RUMContext.IDs.CodingKeys.sessionID.rawValue: "session-id-2", + RUMContext.IDs.CodingKeys.viewID.rawValue: "view-id-2" ], RUMDependency.serverTimeOffsetKey: TimeInterval(345) ] @@ -128,11 +128,3 @@ class RUMContextReceiverTests: XCTestCase { XCTAssertTrue(fallbackCalled) } } - -fileprivate extension RUMDependency { - enum IDs { - static let applicationIDKey = "application_id" - static let sessionIDKey = "session_id" - static let viewIDKey = "view.id" - } -} diff --git a/DatadogSessionReplay/Tests/DatadogCoreIntegration/SRContextPublisherTests.swift b/DatadogSessionReplay/Tests/DatadogCoreIntegration/SRContextPublisherTests.swift index 0ccb90f29b..50151925d5 100644 --- a/DatadogSessionReplay/Tests/DatadogCoreIntegration/SRContextPublisherTests.swift +++ b/DatadogSessionReplay/Tests/DatadogCoreIntegration/SRContextPublisherTests.swift @@ -6,13 +6,55 @@ import XCTest @testable import DatadogSessionReplay +import TestUtilities -// swiftlint:disable empty_xctest_method class SRContextPublisherTests: XCTestCase { - func testItSetsHasReplayAccordingly() { - // TODO: RUMM-2690 - // Implementing this test requires creating mocks for `DatadogCore` and `DatadogContext`, - // which is yet not possible as we lack separate, shared module to facilitate tests. + func testItSetsHasReplayAccordingly() throws { + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + + srContextPublisher.setHasReplay(true) + + let hasReplay = try XCTUnwrap(core.hasReplay) + XCTAssertTrue(hasReplay) + } + + func testItSetsRecordsCountAccordingly() { + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + + let recordsCountByViewID: [String: Int64] = ["view-id": 2] + srContextPublisher.setRecordsCountByViewID(recordsCountByViewID) + + XCTAssertEqual(core.recordsCountByViewID, recordsCountByViewID) + } + + func testItDoesNotOverridePreviouslySetValue() throws { + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let recordsCountByViewID: [String: Int64] = ["view-id": 2] + + srContextPublisher.setHasReplay(true) + srContextPublisher.setRecordsCountByViewID(recordsCountByViewID) + + XCTAssertEqual(core.recordsCountByViewID, recordsCountByViewID) + let hasReplay = try XCTUnwrap(core.hasReplay) + XCTAssertTrue(hasReplay) + + srContextPublisher.setHasReplay(false) + + let hasReplay2 = try XCTUnwrap(core.hasReplay) + XCTAssertFalse(hasReplay2) + XCTAssertEqual(core.recordsCountByViewID, recordsCountByViewID) + } +} + +fileprivate extension PassthroughCoreMock { + var hasReplay: Bool? { + return context.featuresAttributes["session-replay"]?.has_replay + } + + var recordsCountByViewID: [String: Int64]? { + return context.featuresAttributes["session-replay"]?.records_count_by_view_id } } -// swiftlint:enable empty_xctest_method diff --git a/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift b/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift index c6965f7ccf..c14af8a896 100644 --- a/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/SRDataModelsMocks.swift @@ -309,6 +309,137 @@ extension SRContentClip: AnyMockable, RandomMockable { } } +extension SRPlaceholderWireframe: AnyMockable, RandomMockable { + public static func mockAny() -> SRPlaceholderWireframe { + return SRPlaceholderWireframe( + clip: .mockAny(), + height: .mockAny(), + id: .mockAny(), + width: .mockAny(), + x: .mockAny(), + y: .mockAny() + ) + } + + public static func mockRandom() -> SRPlaceholderWireframe { + return SRPlaceholderWireframe( + clip: .mockRandom(), + height: .mockRandom(), + id: .mockRandom(), + width: .mockRandom(), + x: .mockRandom(), + y: .mockRandom() + ) + } + + static func mockWith( + clip: SRContentClip? = .mockAny(), + height: Int64 = .mockAny(), + id: Int64 = .mockAny(), + width: Int64 = .mockAny(), + x: Int64 = .mockAny(), + y: Int64 = .mockAny() + ) -> SRPlaceholderWireframe { + return SRPlaceholderWireframe( + clip: clip, + height: height, + id: id, + width: width, + x: x, + y: y + ) + } + + static func mockRandomWith(id: WireframeID) -> SRPlaceholderWireframe { + return SRPlaceholderWireframe( + clip: .mockRandom(), + height: .mockRandom(), + id: id, + width: .mockRandom(), + x: .mockRandom(), + y: .mockRandom() + ) + } +} + +extension SRImageWireframe: AnyMockable, RandomMockable { + public static func mockAny() -> SRImageWireframe { + return SRImageWireframe( + base64: .mockAny(), + border: .mockAny(), + clip: .mockAny(), + height: .mockAny(), + id: .mockAny(), + isEmpty: .mockAny(), + mimeType: .mockAny(), + shapeStyle: .mockAny(), + width: .mockAny(), + x: .mockAny(), + y: .mockAny() + ) + } + + public static func mockRandom() -> SRImageWireframe { + return SRImageWireframe( + base64: .mockRandom(), + border: .mockRandom(), + clip: .mockRandom(), + height: .mockRandom(), + id: .mockRandom(), + isEmpty: .mockRandom(), + mimeType: .mockRandom(), + shapeStyle: .mockRandom(), + width: .mockRandom(), + x: .mockRandom(), + y: .mockRandom() + ) + } + + static func mockWith( + base64: String? = .mockAny(), + border: SRShapeBorder? = .mockAny(), + clip: SRContentClip? = .mockAny(), + height: Int64 = .mockAny(), + id: Int64 = .mockAny(), + isEmpty: Bool = .mockAny(), + mimeType: String? = .mockAny(), + shapeStyle: SRShapeStyle? = .mockAny(), + width: Int64 = .mockAny(), + x: Int64 = .mockAny(), + y: Int64 = .mockAny() + ) -> SRImageWireframe { + return SRImageWireframe( + base64: base64, + border: border, + clip: clip, + height: height, + id: id, + isEmpty: isEmpty, + mimeType: mimeType, + shapeStyle: shapeStyle, + width: width, + x: x, + y: y + ) + } + + static func mockRandomWith(id: WireframeID) -> SRImageWireframe { + return SRImageWireframe( + base64: .mockRandom(), + border: .mockRandom(), + clip: .mockRandom(), + height: .mockRandom(), + id: id, + isEmpty: .mockRandom(), + mimeType: .mockRandom(), + shapeStyle: .mockRandom(), + width: .mockRandom(), + x: .mockRandom(), + y: .mockRandom() + ) + } +} + extension SRWireframe: AnyMockable, RandomMockable { public static func mockAny() -> SRWireframe { return .shapeWireframe(value: .mockAny()) diff --git a/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift b/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift index 61f30af588..e170cb5ae1 100644 --- a/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift @@ -15,7 +15,9 @@ class DiffSRWireframes: XCTestCase { let randomID: Int64 = .mockRandom() let wireframes: [SRWireframe] = [ .shapeWireframe(value: .mockWith(id: randomID)), - .textWireframe(value: .mockWith(id: randomID)) + .textWireframe(value: .mockWith(id: randomID)), + .imageWireframe(value: .mockWith(id: randomID)), + .placeholderWireframe(value: .mockWith(id: randomID)) ] wireframes.forEach { XCTAssertEqual($0.id, randomID) } @@ -55,6 +57,32 @@ class DiffSRWireframes: XCTestCase { DDAssertReflectionEqual(result, otherWireframe) } + func testsWhenMergingMutationsToTheOriginalImageWireframe_itShouldProduceTheOtherOne() throws { + // Given + let originalWireframe: SRWireframe = .imageWireframe(value: .mockRandom()) + let otherWireframe: SRWireframe = .imageWireframe(value: .mockRandomWith(id: originalWireframe.id)) + + // When + let mutations = try XCTUnwrap(otherWireframe.mutations(from: originalWireframe), "Failed to compute mutations") + + // Then + let result = try XCTUnwrap(originalWireframe.merge(mutation: mutations), "Failed to merge mutations") + DDAssertReflectionEqual(result, otherWireframe) + } + + func testsWhenMergingMutationsToTheOriginalPlaceholderWireframe_itShouldProduceTheOtherOne() throws { + // Given + let originalWireframe: SRWireframe = .placeholderWireframe(value: .mockRandom()) + let otherWireframe: SRWireframe = .placeholderWireframe(value: .mockRandomWith(id: originalWireframe.id)) + + // When + let mutations = try XCTUnwrap(otherWireframe.mutations(from: originalWireframe), "Failed to compute mutations") + + // Then + let result = try XCTUnwrap(originalWireframe.merge(mutation: mutations), "Failed to merge mutations") + DDAssertReflectionEqual(result, otherWireframe) + } + func testWhenComputingMutationsForWireframesWithDifferentID_itThrows() throws { let randomID: WireframeID = .mockRandom() let otherID: WireframeID = .mockRandom(otherThan: [randomID]) @@ -97,6 +125,8 @@ class DiffSRWireframes: XCTestCase { private typealias TextWireframeUpdate = SRIncrementalSnapshotRecord.Data.MutationData.Updates.TextWireframeUpdate private typealias ShapeWireframeUpdate = SRIncrementalSnapshotRecord.Data.MutationData.Updates.ShapeWireframeUpdate +private typealias ImageWireframeUpdate = SRIncrementalSnapshotRecord.Data.MutationData.Updates.ImageWireframeUpdate +private typealias PlaceholderWireframeUpdate = SRIncrementalSnapshotRecord.Data.MutationData.Updates.PlaceholderWireframeUpdate extension SRWireframe { func merge(mutation: WireframeMutation) -> SRWireframe? { @@ -105,6 +135,10 @@ extension SRWireframe { return .shapeWireframe(value: merge(update: update, into: wireframe)) case let (.textWireframe(wireframe), .textWireframeUpdate(update)): return .textWireframe(value: merge(update: update, into: wireframe)) + case let (.imageWireframe(wireframe), .imageWireframeUpdate(update)): + return .imageWireframe(value: merge(update: update, into: wireframe)) + case let (.placeholderWireframe(wireframe), .placeholderWireframeUpdate(update)): + return .placeholderWireframe(value: merge(update: update, into: wireframe)) default: return nil } @@ -138,4 +172,32 @@ extension SRWireframe { y: update.y ?? wireframe.y ) } + + private func merge(update: ImageWireframeUpdate, into wireframe: SRImageWireframe) -> SRImageWireframe { + return SRImageWireframe( + base64: update.base64 ?? wireframe.base64, + border: update.border ?? wireframe.border, + clip: update.clip ?? wireframe.clip, + height: update.height ?? wireframe.height, + id: update.id, + isEmpty: update.isEmpty ?? wireframe.isEmpty, + mimeType: update.mimeType ?? wireframe.mimeType, + shapeStyle: update.shapeStyle ?? wireframe.shapeStyle, + width: update.width ?? wireframe.width, + x: update.x ?? wireframe.x, + y: update.y ?? wireframe.y + ) + } + + private func merge(update: PlaceholderWireframeUpdate, into wireframe: SRPlaceholderWireframe) -> SRPlaceholderWireframe { + return SRPlaceholderWireframe( + clip: update.clip ?? wireframe.clip, + height: update.height ?? wireframe.height, + id: update.id, + label: update.label ?? wireframe.label, + width: update.width ?? wireframe.width, + x: update.x ?? wireframe.x, + y: update.y ?? wireframe.y + ) + } } diff --git a/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift index b8a53fb547..7f5add5608 100644 --- a/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift @@ -26,7 +26,9 @@ class ProcessorTests: XCTestCase { let rum: RUMContext = .mockWith(serverTimeOffset: 0) // Given - let processor = Processor(queue: NoQueue(), writer: writer) + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher) let viewTree = generateSimpleViewTree() // When @@ -47,6 +49,8 @@ class ProcessorTests: XCTestCase { XCTAssertTrue(enrichedRecord.records[0].isMetaRecord) XCTAssertTrue(enrichedRecord.records[1].isFocusRecord) XCTAssertTrue(enrichedRecord.records[2].isFullSnapshotRecord && enrichedRecord.hasFullSnapshot) + + XCTAssertEqual(core.recordsCountByViewID, ["abc": 3]) } func testWhenRUMContextDoesNotChangeInSucceedingViewTreeSnapshots_itWritesRecordsThatContinueCurrentSegment() { @@ -54,7 +58,9 @@ class ProcessorTests: XCTestCase { let rum: RUMContext = .mockWith(serverTimeOffset: 0) // Given - let processor = Processor(queue: NoQueue(), writer: writer) + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher) let viewTree = generateSimpleViewTree() // When @@ -90,6 +96,8 @@ class ProcessorTests: XCTestCase { XCTAssertEqual(enrichedRecord.earliestTimestamp, expectedTime.timeIntervalSince1970.toInt64Milliseconds) XCTAssertEqual(enrichedRecord.latestTimestamp, expectedTime.timeIntervalSince1970.toInt64Milliseconds) } + + XCTAssertEqual(core.recordsCountByViewID, ["abc": 5]) } func testWhenOrientationChanges_itWritesRecordsViewportResizeDataSegment() { @@ -97,7 +105,9 @@ class ProcessorTests: XCTestCase { let rum: RUMContext = .mockRandom() // Given - let processor = Processor(queue: NoQueue(), writer: writer) + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher) let view = UIView.mock(withFixture: .visible(.someAppearance)) view.frame = CGRect(x: 0, y: 0, width: 100, height: 200) let rotatedView = UIView.mock(withFixture: .visible(.someAppearance)) @@ -124,6 +134,8 @@ class ProcessorTests: XCTestCase { XCTAssertTrue(enrichedRecords[1].records[1].isIncrementalSnapshotRecord) XCTAssertEqual(enrichedRecords[1].records[1].incrementalSnapshot?.viewportResizeData?.height, 100) XCTAssertEqual(enrichedRecords[1].records[1].incrementalSnapshot?.viewportResizeData?.width, 200) + + XCTAssertEqual(core.recordsCountByViewID?.values.first, 5) } func testWhenRUMContextChangesInSucceedingViewTreeSnapshots_itWritesRecordsThatIndicateNextSegments() { @@ -132,7 +144,9 @@ class ProcessorTests: XCTestCase { let rum2: RUMContext = .mockRandom() // Given - let processor = Processor(queue: NoQueue(), writer: writer) + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher) let viewTree = generateSimpleViewTree() // When @@ -171,6 +185,8 @@ class ProcessorTests: XCTestCase { XCTAssertEqual(enrichedRecord.sessionID, expectedRUM.ids.sessionID) XCTAssertEqual(enrichedRecord.viewID, expectedRUM.ids.viewID) } + + XCTAssertEqual(core.recordsCountByViewID?.values.map { $0 }, [4, 4]) } // MARK: - Processing `TouchSnapshots` @@ -182,7 +198,9 @@ class ProcessorTests: XCTestCase { let rum: RUMContext = .mockWith(serverTimeOffset: 0) // Given - let processor = Processor(queue: NoQueue(), writer: writer) + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher) // When let touchSnapshot = generateTouchSnapshot(startAt: earliestTouchTime, endAt: snapshotTime, numberOfTouches: numberOfTouches) @@ -215,6 +233,8 @@ class ProcessorTests: XCTestCase { XCTAssertGreaterThanOrEqual(record.timestamp, earliestTouchTime.timeIntervalSince1970.toInt64Milliseconds) XCTAssertLessThanOrEqual(record.timestamp, snapshotTime.timeIntervalSince1970.toInt64Milliseconds) } + + XCTAssertEqual(core.recordsCountByViewID, ["abc": 13]) } func testWhenRUMContextTimeOffsetChangesInSucceedingViewTreeSnapshots_itWritesRecordsThatContinueCurrentSegment() { @@ -223,7 +243,9 @@ class ProcessorTests: XCTestCase { let rum2: RUMContext = .mockWith(serverTimeOffset: 456) // Given - let processor = Processor(queue: NoQueue(), writer: writer) + let core = PassthroughCoreMock() + let srContextPublisher = SRContextPublisher(core: core) + let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher) let viewTree = generateSimpleViewTree() // When @@ -250,6 +272,8 @@ class ProcessorTests: XCTestCase { XCTAssertEqual(enrichedRecord.sessionID, expectedRUM.ids.sessionID) XCTAssertEqual(enrichedRecord.viewID, expectedRUM.ids.viewID) } + + XCTAssertEqual(core.recordsCountByViewID, ["abc": 4]) } // MARK: - `ViewTreeSnapshot` generation @@ -293,3 +317,9 @@ class ProcessorTests: XCTestCase { ) } } + +fileprivate extension PassthroughCoreMock { + var recordsCountByViewID: [String: Int64]? { + return context.featuresAttributes["session-replay"]?.records_count_by_view_id + } +} diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift index 6388b5acfb..9adefbd958 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -50,10 +50,10 @@ class UIImageViewWireframesBuilderTests: XCTestCase { func test_BuildCorrectWireframes_whenContentImageIsIgnored() { let wireframeID = WireframeID.mockRandom() - let imageWireframeID = WireframeID.mockRandom() + let placeholderWireframeID = WireframeID.mockRandom() let builder = UIImageViewWireframesBuilder( wireframeID: wireframeID, - imageWireframeID: imageWireframeID, + imageWireframeID: placeholderWireframeID, attributes: ViewAttributes.mock(fixture: .visible(.someAppearance)), contentFrame: CGRect(x: 10, y: 10, width: 200, height: 200), clipsToBounds: true, @@ -73,9 +73,9 @@ class UIImageViewWireframesBuilderTests: XCTestCase { XCTFail("First wireframe needs to be shapeWireframe case") } - if case let .imageWireframe(imageWireframe) = wireframes[1] { - XCTAssertEqual(imageWireframe.id, imageWireframeID) - XCTAssertEqual(imageWireframe.base64, "") + if case let .placeholderWireframe(placeholderWireframe) = wireframes[1] { + XCTAssertEqual(placeholderWireframe.id, placeholderWireframeID) + XCTAssertEqual(placeholderWireframe.label, "Content Image") } else { XCTFail("Second wireframe needs to be imageWireframe case") } diff --git a/Sources/Datadog/Core/PerformancePreset.swift b/Sources/Datadog/Core/PerformancePreset.swift index e13322235d..1eb168c4cf 100644 --- a/Sources/Datadog/Core/PerformancePreset.swift +++ b/Sources/Datadog/Core/PerformancePreset.swift @@ -80,12 +80,11 @@ internal extension PerformancePreset { } }() - let uploadDelayFactors: (initial: Double, default: Double, min: Double, max: Double, changeRate: Double) = { + let uploadDelayFactors: (initial: Double, min: Double, max: Double, changeRate: Double) = { switch bundleType { case .iOSApp: return ( initial: 5, - default: 5, min: 1, max: 10, changeRate: 0.1 @@ -93,7 +92,6 @@ internal extension PerformancePreset { case .iOSAppExtension: return ( initial: 0.5, // ensures the the first upload is checked quickly after starting the short-lived app extension - default: 3, min: 1, max: 5, changeRate: 0.5 // if batches are found, reduces interval significantly for more uploads in short-lived app extension @@ -111,7 +109,7 @@ internal extension PerformancePreset { init( meanFileAge: TimeInterval, minUploadDelay: TimeInterval, - uploadDelayFactors: (initial: Double, default: Double, min: Double, max: Double, changeRate: Double) + uploadDelayFactors: (initial: Double, min: Double, max: Double, changeRate: Double) ) { self.maxFileSize = UInt64(4).MB self.maxDirectorySize = UInt64(512).MB @@ -126,19 +124,19 @@ internal extension PerformancePreset { self.uploadDelayChangeRate = uploadDelayFactors.changeRate } - func updated(with: PerformancePresetOverride) -> PerformancePreset { + func updated(with override: PerformancePresetOverride) -> PerformancePreset { return PerformancePreset( - maxFileSize: with.maxFileSize ?? maxFileSize, + maxFileSize: override.maxFileSize ?? maxFileSize, maxDirectorySize: maxDirectorySize, - maxFileAgeForWrite: maxFileAgeForWrite, - minFileAgeForRead: minFileAgeForRead, + maxFileAgeForWrite: override.maxFileAgeForWrite ?? maxFileAgeForWrite, + minFileAgeForRead: override.minFileAgeForRead ?? minFileAgeForRead, maxFileAgeForRead: maxFileAgeForRead, maxObjectsInFile: maxObjectsInFile, - maxObjectSize: with.maxObjectSize ?? maxObjectSize, - initialUploadDelay: initialUploadDelay, - minUploadDelay: minUploadDelay, - maxUploadDelay: maxUploadDelay, - uploadDelayChangeRate: uploadDelayChangeRate + maxObjectSize: override.maxObjectSize ?? maxObjectSize, + initialUploadDelay: override.initialUploadDelay ?? initialUploadDelay, + minUploadDelay: override.minUploadDelay ?? minUploadDelay, + maxUploadDelay: override.maxUploadDelay ?? maxUploadDelay, + uploadDelayChangeRate: override.uploadDelayChangeRate ?? uploadDelayChangeRate ) } } diff --git a/Sources/Datadog/DatadogCore/DatadogCore.swift b/Sources/Datadog/DatadogCore/DatadogCore.swift index 3451b1a6bd..8a07f3e428 100644 --- a/Sources/Datadog/DatadogCore/DatadogCore.swift +++ b/Sources/Datadog/DatadogCore/DatadogCore.swift @@ -373,6 +373,16 @@ extension DatadogCore: DatadogCoreProtocol { contextProvider.write { $0.featuresAttributes[feature] = attributes() } } + /* public */ func update(feature: String, attributes: @escaping () -> FeatureBaggage) { + contextProvider.write { + if $0.featuresAttributes[feature] != nil { + $0.featuresAttributes[feature]?.merge(with: attributes()) + } else { + $0.featuresAttributes[feature] = attributes() + } + } + } + /* public */ func send(message: FeatureMessage, sender: DatadogCoreProtocol, else fallback: @escaping () -> Void) { messageBusQueue.async { let receivers = self.messageBus.values.filter { diff --git a/Sources/Datadog/DatadogCore/Storage/DataBlock.swift b/Sources/Datadog/DatadogCore/Storage/DataBlock.swift index b5fcccff95..edbf0fb1d1 100644 --- a/Sources/Datadog/DatadogCore/Storage/DataBlock.swift +++ b/Sources/Datadog/DatadogCore/Storage/DataBlock.swift @@ -14,7 +14,11 @@ private let MAX_DATA_LENGTH: UInt64 = 10 * 1_024 * 1_024 /// Block type supported in data stream internal enum BlockType: UInt16 { + /// Represents an event case event = 0x00 + /// Represents an event metadata associated with the previous event. + /// This block is optional and may be omitted. + case eventMetadata = 0x01 } /// Reported errors while manipulating data blocks. diff --git a/Sources/Datadog/DatadogCore/Storage/Reading/FileReader.swift b/Sources/Datadog/DatadogCore/Storage/Reading/FileReader.swift index a41e5b7876..1025bddf4a 100644 --- a/Sources/Datadog/DatadogCore/Storage/Reading/FileReader.swift +++ b/Sources/Datadog/DatadogCore/Storage/Reading/FileReader.swift @@ -31,8 +31,8 @@ internal final class FileReader: Reader { } do { - let events = try decode(stream: file.stream()) - return Batch(events: events, file: file) + let dataBlocks = try decode(stream: file.stream()) + return Batch(dataBlocks: dataBlocks, file: file) } catch { DD.telemetry.error("Failed to read data from file", error: error) return nil @@ -47,7 +47,7 @@ internal final class FileReader: Reader { /// /// - Parameter stream: The InputStream that provides data to decode. /// - Returns: The decoded and formatted data. - private func decode(stream: InputStream) throws -> [Data] { + private func decode(stream: InputStream) throws -> [DataBlock] { let reader = DataBlockReader( input: stream, maxBlockLength: orchestrator.performance.maxObjectSize @@ -59,17 +59,9 @@ internal final class FileReader: Reader { } return try reader.all() - // get event blocks only - .compactMap { - switch $0.type { - case .event: - return $0.data - } - } - // decrypt data - report failure - .compactMap { (data: Data) in + .compactMap { dataBlock in do { - return try decrypt(data: data) + return try decrypt(dataBlock: dataBlock) } catch { failure = "πŸ”₯ Failed to decrypt data with error: \(error)" return nil @@ -77,6 +69,11 @@ internal final class FileReader: Reader { } } + private func decrypt(dataBlock: DataBlock) throws -> DataBlock { + let decrypted = try decrypt(data: dataBlock.data) + return DataBlock(type: dataBlock.type, data: decrypted) + } + /// Decrypts data if encryption is available. /// /// If no encryption, the data is returned. diff --git a/Sources/Datadog/DatadogCore/Storage/Reading/Reader.swift b/Sources/Datadog/DatadogCore/Storage/Reading/Reader.swift index 9669bfac51..ef22ee91a9 100644 --- a/Sources/Datadog/DatadogCore/Storage/Reading/Reader.swift +++ b/Sources/Datadog/DatadogCore/Storage/Reading/Reader.swift @@ -7,11 +7,20 @@ import Foundation internal struct Batch { - let events: [Data] + /// Data blocks in the batch. + let dataBlocks: [DataBlock] /// File from which `data` was read. let file: ReadableFile } +extension Batch { + /// Events contained in the batch. + var events: [Event] { + let generator = EventGenerator(dataBlocks: dataBlocks) + return generator.map { $0 } + } +} + /// A type, reading batched data. internal protocol Reader { func readNextBatch() -> Batch? diff --git a/Sources/Datadog/DatadogCore/Storage/Writing/FileWriter.swift b/Sources/Datadog/DatadogCore/Storage/Writing/FileWriter.swift index 27b3c7d155..f317f04f73 100644 --- a/Sources/Datadog/DatadogCore/Storage/Writing/FileWriter.swift +++ b/Sources/Datadog/DatadogCore/Storage/Writing/FileWriter.swift @@ -20,13 +20,28 @@ internal struct FileWriter: Writer { // MARK: - Writing data - /// Encodes given value to JSON data and writes it to the file. - func write(value: T) { + /// Encodes given encodable value and metadata, and writes it to the file. + /// If encryption is available, the data is encrypted before writing. + /// - Parameters: + /// - value: Encodable value to write. + /// - metadata: Encodable metadata to write. + func write(value: T, metadata: M?) { do { - let data = try encode(event: value) - let writeSize = UInt64(data.count) + var encoded: Data = .init() + if let metadata = metadata { + let encodedMetadata = try encode(encodable: metadata, blockType: .eventMetadata) + encoded.append(encodedMetadata) + } + + let encodedValue = try encode(encodable: value, blockType: .event) + encoded.append(encodedValue) + + // Make sure both event and event metadata are written to the same file. + // This is to avoid a situation where event is written to one file and event metadata to another. + // If this happens, the reader will not be able to match event with its metadata. + let writeSize = UInt64(encoded.count) let file = try forceNewFile ? orchestrator.getNewWritableFile(writeSize: writeSize) : orchestrator.getWritableFile(writeSize: writeSize) - try file.append(data: data) + try file.append(data: encoded) } catch { DD.logger.error("Failed to write data", error: error) DD.telemetry.error("Failed to write data to file", error: error) @@ -46,10 +61,10 @@ internal struct FileWriter: Writer { /// /// - Parameter event: The value to encode. /// - Returns: Data representation of the value. - private func encode(event: T) throws -> Data { - let data = try jsonEncoder.encode(event) + private func encode(encodable: Encodable, blockType: BlockType) throws -> Data { + let data = try jsonEncoder.encode(encodable) return try DataBlock( - type: .event, + type: blockType, data: encrypt(data: data) ).serialize( maxLength: orchestrator.performance.maxObjectSize diff --git a/Sources/Datadog/DatadogCore/Storage/Writing/Writer.swift b/Sources/Datadog/DatadogCore/Storage/Writing/Writer.swift index 5004c11c25..46be94373d 100644 --- a/Sources/Datadog/DatadogCore/Storage/Writing/Writer.swift +++ b/Sources/Datadog/DatadogCore/Storage/Writing/Writer.swift @@ -8,7 +8,20 @@ import Foundation /// A type, writing data. public protocol Writer { - func write(value: T) + /// Encodes given encodable value and metadata, and writes to the destination. + /// - Parameter value: Encodable value to write. + /// - Parameter metadata: Encodable metadata to write. + func write(value: T, metadata: M?) +} + +extension Writer { + /// Encodes given encodable value and writes to the destination. + /// Uses `write(value:metadata:)` with `nil` metadata. + /// - Parameter value: Encodable value to write. + public func write(value: T) { + let metadata: Data? = nil + write(value: value, metadata: metadata) + } } /// Writer performing writes asynchronously on a given queue. @@ -21,11 +34,12 @@ internal struct AsyncWriter: Writer { self.queue = queue } - func write(value: T) where T: Encodable { - queue.async { writer.write(value: value) } + func write(value: T, metadata: M?) { + queue.async { writer.write(value: value, metadata: metadata) } } } internal struct NOPWriter: Writer { - func write(value: T) where T: Encodable {} + func write(value: T, metadata: M?) { + } } diff --git a/Sources/Datadog/DatadogCore/Upload/DataUploader.swift b/Sources/Datadog/DatadogCore/Upload/DataUploader.swift index 6a4c21f4e9..624d814703 100644 --- a/Sources/Datadog/DatadogCore/Upload/DataUploader.swift +++ b/Sources/Datadog/DatadogCore/Upload/DataUploader.swift @@ -8,7 +8,7 @@ import Foundation /// A type that performs data uploads. internal protocol DataUploaderType { - func upload(events: [Data], context: DatadogContext) throws -> DataUploadStatus + func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus } /// Synchronously uploads data to server using `HTTPClient`. @@ -26,7 +26,7 @@ internal final class DataUploader: DataUploaderType { /// Uploads data synchronously (will block current thread) and returns the upload status. /// Uses timeout configured for `HTTPClient`. - func upload(events: [Data], context: DatadogContext) throws -> DataUploadStatus { + func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus { let request = try requestBuilder.request(for: events, with: context) let requestID = request.value(forHTTPHeaderField: URLRequestBuilder.HTTPHeader.ddRequestIDHeaderField) diff --git a/Sources/Datadog/DatadogInternal/DatadogCoreProtocol.swift b/Sources/Datadog/DatadogInternal/DatadogCoreProtocol.swift index 63a233fcbe..6d1d099039 100644 --- a/Sources/Datadog/DatadogInternal/DatadogCoreProtocol.swift +++ b/Sources/Datadog/DatadogInternal/DatadogCoreProtocol.swift @@ -97,6 +97,36 @@ public protocol DatadogCoreProtocol: AnyObject { /// - attributes: The Feature's attributes to set. func set(feature: String, attributes: @escaping () -> FeatureBaggage) + /// Updates given attributes for a given Feature for sharing data through `DatadogContext`. + /// + /// This method provides a passive communication chanel between Features of the Core. + /// For an active Feature-to-Feature communication, please use the `send(message:)` + /// method. + /// + /// Updating attributes will update the Core Context and will be shared across Features. + /// In the following examples, the Feature `foo` will update two attributes and a second + /// Feature `bar` will read them through the event write context. + /// This function does not remove item if `nil` is provided as a value. + /// + /// // Foo.swift + /// core.update(feature: "foo", attributes: [ + /// "id": 1 + /// ]) + /// core.update(feature: "foo", attributes: [ + /// "name": "bazz" + /// ]) + /// + /// // Bar.swift + /// core.scope(for: "bar").eventWriteContext { context, writer in + /// let fooID: Int? = context.featuresAttributes["foo"]?.id + /// let fooName: String? = context.featuresAttributes["foo"]?.name + /// } + /// + /// - Parameters: + /// - feature: The Feature's name. + /// - attributes: The Feature's attributes to set. + func update(feature: String, attributes: @escaping () -> FeatureBaggage) + /// Sends a message on the bus shared by features registered in this core. /// /// If the message could not be processed by any registered feature, the fallback closure @@ -214,5 +244,7 @@ internal class NOPDatadogCore: DatadogCoreProtocol { /// no-op func set(feature: String, attributes: @escaping @autoclosure () -> FeatureBaggage) { } /// no-op + func update(feature: String, attributes: @escaping () -> FeatureBaggage) { } + /// no-op func send(message: FeatureMessage, sender: DatadogCoreProtocol, else fallback: @escaping () -> Void) { } } diff --git a/Sources/Datadog/DatadogInternal/MessageBus/FeatureBaggage.swift b/Sources/Datadog/DatadogInternal/MessageBus/FeatureBaggage.swift index e696fd31cc..925015ea20 100644 --- a/Sources/Datadog/DatadogInternal/MessageBus/FeatureBaggage.swift +++ b/Sources/Datadog/DatadogInternal/MessageBus/FeatureBaggage.swift @@ -252,6 +252,13 @@ public struct FeatureBaggage { get { try? value(forKey: key, type: T.self) } set { try? updateValue(newValue, forKey: key) } } + + /// Merges with the values of another FeatureBaggage. + /// + /// - Parameter baggage: The FeatureBaggage to merge. + public mutating func merge(with baggage: FeatureBaggage) { + attributes.merge(baggage.attributes) { $1 } + } } extension FeatureBaggage: ExpressibleByDictionaryLiteral { diff --git a/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift b/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift index 426f974fc3..653a155c7d 100644 --- a/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift +++ b/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift @@ -10,22 +10,74 @@ import Foundation /// performance presets by setting optional limits. If the limits are not provided, the default values from /// the `PerformancePreset` object will be used. public struct PerformancePresetOverride { - /// An optional value representing the maximum allowed file size in bytes. + /// Overrides the the maximum allowed file size in bytes. /// If not provided, the default value from the `PerformancePreset` object is used. let maxFileSize: UInt64? - /// An optional value representing the maximum allowed object size in bytes. + /// Overrides the maximum allowed object size in bytes. /// If not provided, the default value from the `PerformancePreset` object is used. let maxObjectSize: UInt64? - /// Initializes a new `PerformancePresetOverride` instance with the provided - /// maximum file size and maximum object size limits. + /// Overrides the maximum age qualifying given file for reuse (in seconds). + /// If recently used file is younger than this, it is reused - otherwise: new file is created. + let maxFileAgeForWrite: TimeInterval? + + /// Minimum age qualifying given file for upload (in seconds). + /// If the file is older than this, it is uploaded (and then deleted if upload succeeded). + /// It has an arbitrary offset (~0.5s) over `maxFileAgeForWrite` to ensure that no upload can start for the file being currently written. + let minFileAgeForRead: TimeInterval? + + /// Overrides the initial upload delay (in seconds). + /// At runtime, the upload interval starts with `initialUploadDelay` and then ranges from `minUploadDelay` to `maxUploadDelay` depending + /// on delivery success or failure. + let initialUploadDelay: TimeInterval? + + /// Overrides the mininum interval of data upload (in seconds). + let minUploadDelay: TimeInterval? + + /// Overrides the maximum interval of data upload (in seconds). + let maxUploadDelay: TimeInterval? + + /// Overrides the current interval is change on successful upload. Should be less or equal `1.0`. + /// E.g: if rate is `0.1` then `delay` will be changed by `delay * 0.1`. + let uploadDelayChangeRate: Double? + + /// Initializes a new `PerformancePresetOverride` instance with the provided overrides. /// /// - Parameters: /// - maxFileSize: The maximum allowed file size in bytes, or `nil` to use the default value from `PerformancePreset`. /// - maxObjectSize: The maximum allowed object size in bytes, or `nil` to use the default value from `PerformancePreset`. - public init(maxFileSize: UInt64?, maxObjectSize: UInt64?) { + /// - meanFileAge: The mean age qualifying a file for reuse, or `nil` to use the default value from `PerformancePreset`. + /// - minUploadDelay: The mininum interval of data uploads, or `nil` to use the default value from `PerformancePreset`. + public init( + maxFileSize: UInt64?, + maxObjectSize: UInt64?, + meanFileAge: TimeInterval?, + minUploadDelay: TimeInterval? + ) { self.maxFileSize = maxFileSize self.maxObjectSize = maxObjectSize + + if let meanFileAge { + // Following constants are the same as in `DatadogCore.PerformancePreset` + self.maxFileAgeForWrite = meanFileAge * 0.95 // 5% below the mean age + self.minFileAgeForRead = meanFileAge * 1.05 // 5% above the mean age + } else { + self.maxFileAgeForWrite = nil + self.minFileAgeForRead = nil + } + + if let minUploadDelay { + // Following constants are the same as in `DatadogCore.PerformancePreset` + self.initialUploadDelay = minUploadDelay * 5 + self.minUploadDelay = minUploadDelay + self.maxUploadDelay = minUploadDelay * 10 + self.uploadDelayChangeRate = 0.1 + } else { + self.initialUploadDelay = nil + self.minUploadDelay = nil + self.maxUploadDelay = nil + self.uploadDelayChangeRate = nil + } } } diff --git a/Sources/Datadog/DatadogInternal/Upload/Event.swift b/Sources/Datadog/DatadogInternal/Upload/Event.swift new file mode 100644 index 0000000000..c36ed24fc6 --- /dev/null +++ b/Sources/Datadog/DatadogInternal/Upload/Event.swift @@ -0,0 +1,76 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// Struct representing a single event. +public struct Event: Equatable { + /// Data representing the event. + public let data: Data + + /// Metadata associated with the event. + /// Metadata is optional and may be `nil` but of very small size. + /// This allows us to skip resource intensive operations in case such + /// as filtering of the events. + public let metadata: Data? + + public init(data: Data, metadata: Data? = nil) { + self.data = data + self.metadata = metadata + } +} + +/// Event generator that generates events from the given data blocks. +internal struct EventGenerator: Sequence, IteratorProtocol { + private let dataBlocks: [DataBlock] + private var index: Int + + init(dataBlocks: [DataBlock], index: Int = 0) { + self.dataBlocks = dataBlocks + self.index = index + } + + /// Returns the next event. + /// + /// Data format + /// ``` + /// [EVENT 1 METADATA] [EVENT 1] [EVENT 2 METADATA] [EVENT 2] [EVENT 3] + /// ``` + /// + /// - Returns: The next event or `nil` if there are no more events. + /// - Note: a `DataBlock` with `.event` type marks the beginning of the event. + /// It is either followed by another `DataBlock` with `.event` type or + /// by a `DataBlock` with `.metadata` type. + mutating func next() -> Event? { + guard index < dataBlocks.count else { + return nil + } + + var metadata: DataBlock? = nil + // If the next block is an event metadata, read it. + if dataBlocks[index].type == .eventMetadata { + metadata = dataBlocks[index] + index += 1 + } + + // If this is the last block, return nil. + // there cannot be a metadata block without an event block. + guard index < dataBlocks.count else { + return nil + } + + // If the next block is an event, read it. + guard dataBlocks[index].type == .event else { + // this is safeguard against corrupted data. + // if there was a metadata block, it will be skipped. + return next() + } + let event = dataBlocks[index] + index += 1 + + return Event(data: event.data, metadata: metadata?.data) + } +} diff --git a/Sources/Datadog/DatadogInternal/Upload/FeatureRequestBuilder.swift b/Sources/Datadog/DatadogInternal/Upload/FeatureRequestBuilder.swift index 87eb3059ac..80ac415d77 100644 --- a/Sources/Datadog/DatadogInternal/Upload/FeatureRequestBuilder.swift +++ b/Sources/Datadog/DatadogInternal/Upload/FeatureRequestBuilder.swift @@ -25,5 +25,5 @@ public protocol FeatureRequestBuilder { /// - context: The current core context. /// - events: The events data to be uploaded. /// - Returns: The URL request. - func request(for events: [Data], with context: DatadogContext) throws -> URLRequest + func request(for events: [Event], with context: DatadogContext) throws -> URLRequest } diff --git a/Sources/Datadog/Logging/Log/LogEventSanitizer.swift b/Sources/Datadog/Logging/Log/LogEventSanitizer.swift index 6c8d2b913d..7478579f46 100644 --- a/Sources/Datadog/Logging/Log/LogEventSanitizer.swift +++ b/Sources/Datadog/Logging/Log/LogEventSanitizer.swift @@ -13,7 +13,7 @@ internal struct LogEventSanitizer { /// If any of those is used by the user, the attribute will be ignored. static let reservedAttributeNames: Set = [ "host", "message", "status", "service", "source", "ddtags", - "dd.trace_id", "dd.span_id", + "dd.trace_id", "dd.span_id", "application.id", "session.id", "application_id", "session_id", "view.id", "user_action.id", ] /// Allowed first character of a tag name (given as ASCII values ranging from lowercased `a` to `z`) . diff --git a/Sources/Datadog/Logging/LoggingV2Configuration.swift b/Sources/Datadog/Logging/LoggingV2Configuration.swift index e91f67b4ac..b6bb093618 100644 --- a/Sources/Datadog/Logging/LoggingV2Configuration.swift +++ b/Sources/Datadog/Logging/LoggingV2Configuration.swift @@ -27,6 +27,17 @@ internal func createLoggingConfiguration( ) } +internal func mapInternalAttributeKey(_ originalAttribute: String) -> String { + switch originalAttribute { + case "application.id": + return "application_id" + case "session.id": + return "session_id" + default: + return originalAttribute + } +} + /// The Logging URL Request Builder for formatting and configuring the `URLRequest` /// to upload logs data. internal struct LoggingRequestBuilder: FeatureRequestBuilder { @@ -36,7 +47,7 @@ internal struct LoggingRequestBuilder: FeatureRequestBuilder { /// The logs request body format. let format = DataFormat(prefix: "[", suffix: "]", separator: ",") - func request(for events: [Data], with context: DatadogContext) -> URLRequest { + func request(for events: [Event], with context: DatadogContext) -> URLRequest { let builder = URLRequestBuilder( url: intake, queryItems: [ @@ -56,7 +67,7 @@ internal struct LoggingRequestBuilder: FeatureRequestBuilder { ] ) - let data = format.format(events) + let data = format.format(events.map { $0.data }) return builder.uploadRequest(with: data) } } @@ -286,7 +297,8 @@ internal struct WebViewLogReceiver: FeatureMessageReceiver { } if let baggage: [String: String?] = context.featuresAttributes["rum"]?.ids { - event.merge(baggage as [String: Any]) { $1 } + let mappedBaggage = Dictionary(uniqueKeysWithValues: baggage.map { key, value in (mapInternalAttributeKey(key), value) }) + event.merge(mappedBaggage as [String: Any]) { $1 } } writer.write(value: AnyEncodable(event)) diff --git a/Sources/Datadog/Logging/RemoteLogger.swift b/Sources/Datadog/Logging/RemoteLogger.swift index 2dd507d8df..2c98546943 100644 --- a/Sources/Datadog/Logging/RemoteLogger.swift +++ b/Sources/Datadog/Logging/RemoteLogger.swift @@ -129,7 +129,8 @@ internal final class RemoteLogger: LoggerProtocol { if self.rumContextIntegration, let attributes: [String: AnyCodable?] = contextAttributes["rum"]?.ids { let attributes = attributes.compactMapValues { $0 } - internalAttributes.merge(attributes) { $1 } + let mappedAttributes = Dictionary(uniqueKeysWithValues: attributes.map { key, value in (mapInternalAttributeKey(key), value) }) + internalAttributes.merge(mappedAttributes) { $1 } } if self.activeSpanIntegration, let attributes = contextAttributes["tracing"] { diff --git a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift index db44f7b0ca..75a59d0022 100644 --- a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift +++ b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift @@ -35,7 +35,7 @@ public struct RUMActionEvent: RUMDataModel { public let device: RUMDevice? /// Display properties - public let display: RUMDisplay? + public let display: Display? /// Operating system properties public let os: RUMOperatingSystem? @@ -154,14 +154,14 @@ public struct RUMActionEvent: RUMDataModel { /// Session-related internal properties public struct Session: Codable { - /// Session plan: 1 is the plan without replay, 2 is the plan with replay - public let plan: Plan + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? enum CodingKeys: String, CodingKey { case plan = "plan" } - /// Session plan: 1 is the plan without replay, 2 is the plan with replay + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) public enum Plan: Int, Codable { case plan1 = 1 case plan2 = 2 @@ -300,6 +300,30 @@ public struct RUMActionEvent: RUMDataModel { } } + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + /// Session properties public struct Session: Codable { /// Whether this session has a replay @@ -407,7 +431,7 @@ public struct RUMErrorEvent: RUMDataModel { public let device: RUMDevice? /// Display properties - public let display: RUMDisplay? + public let display: Display? /// Error properties public var error: Error @@ -484,14 +508,14 @@ public struct RUMErrorEvent: RUMDataModel { /// Session-related internal properties public struct Session: Codable { - /// Session plan: 1 is the plan without replay, 2 is the plan with replay - public let plan: Plan + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? enum CodingKeys: String, CodingKey { case plan = "plan" } - /// Session plan: 1 is the plan without replay, 2 is the plan with replay + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) public enum Plan: Int, Codable { case plan1 = 1 case plan2 = 2 @@ -519,11 +543,38 @@ public struct RUMErrorEvent: RUMDataModel { } } + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + /// Error properties public struct Error: Codable { /// Causes of the error public var causes: [Causes]? + /// Fingerprint used for Error Tracking custom grouping + public var fingerprint: String? + /// Whether the error has been handled manually in the source code or not public let handling: Handling? @@ -556,6 +607,7 @@ public struct RUMErrorEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case causes = "causes" + case fingerprint = "fingerprint" case handling = "handling" case handlingStack = "handling_stack" case id = "id" @@ -825,7 +877,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let device: RUMDevice? /// Display properties - public let display: RUMDisplay? + public let display: Display? /// Long Task properties public let longTask: LongTask @@ -902,14 +954,14 @@ public struct RUMLongTaskEvent: RUMDataModel { /// Session-related internal properties public struct Session: Codable { - /// Session plan: 1 is the plan without replay, 2 is the plan with replay - public let plan: Plan + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? enum CodingKeys: String, CodingKey { case plan = "plan" } - /// Session plan: 1 is the plan without replay, 2 is the plan with replay + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) public enum Plan: Int, Codable { case plan1 = 1 case plan2 = 2 @@ -937,6 +989,30 @@ public struct RUMLongTaskEvent: RUMDataModel { } } + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + /// Long Task properties public struct LongTask: Codable { /// Duration in ns of the long task @@ -1058,7 +1134,7 @@ public struct RUMResourceEvent: RUMDataModel { public let device: RUMDevice? /// Display properties - public let display: RUMDisplay? + public let display: Display? /// Operating system properties public let os: RUMOperatingSystem? @@ -1147,14 +1223,14 @@ public struct RUMResourceEvent: RUMDataModel { /// Session-related internal properties public struct Session: Codable { - /// Session plan: 1 is the plan without replay, 2 is the plan with replay - public let plan: Plan + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? enum CodingKeys: String, CodingKey { case plan = "plan" } - /// Session plan: 1 is the plan without replay, 2 is the plan with replay + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) public enum Plan: Int, Codable { case plan1 = 1 case plan2 = 2 @@ -1182,6 +1258,30 @@ public struct RUMResourceEvent: RUMDataModel { } } + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + /// Resource properties public struct Resource: Codable { /// Connect phase properties @@ -1194,7 +1294,7 @@ public struct RUMResourceEvent: RUMDataModel { public let download: Download? /// Duration of the resource - public let duration: Int64 + public let duration: Int64? /// First Byte phase properties public let firstByte: FirstByte? @@ -1479,7 +1579,7 @@ public struct RUMViewEvent: RUMDataModel { public let device: RUMDevice? /// Display properties - public let display: RUMDisplay? + public let display: Display? /// Feature flags properties public internal(set) var featureFlags: FeatureFlags? @@ -1487,6 +1587,9 @@ public struct RUMViewEvent: RUMDataModel { /// Operating system properties public let os: RUMOperatingSystem? + /// Privacy properties + public let privacy: Privacy? + /// The service name for this application public let service: String? @@ -1522,6 +1625,7 @@ public struct RUMViewEvent: RUMDataModel { case display = "display" case featureFlags = "feature_flags" case os = "os" + case privacy = "privacy" case service = "service" case session = "session" case source = "source" @@ -1543,6 +1647,12 @@ public struct RUMViewEvent: RUMDataModel { /// Version of the RUM event format public let formatVersion: Int64 = 2 + /// List of the page states during the view + public let pageStates: [PageStates]? + + /// Debug metadata for Replay Sessions + public let replayStats: ReplayStats? + /// Session-related internal properties public let session: Session? @@ -1550,19 +1660,62 @@ public struct RUMViewEvent: RUMDataModel { case browserSdkVersion = "browser_sdk_version" case documentVersion = "document_version" case formatVersion = "format_version" + case pageStates = "page_states" + case replayStats = "replay_stats" case session = "session" } + /// Properties of the page state + public struct PageStates: Codable { + /// Duration in ns between start of the view and start of the page state + public let start: Int64 + + /// Page state name + public let state: State + + enum CodingKeys: String, CodingKey { + case start = "start" + case state = "state" + } + + /// Page state name + public enum State: String, Codable { + case active = "active" + case passive = "passive" + case hidden = "hidden" + case frozen = "frozen" + case terminated = "terminated" + } + } + + /// Debug metadata for Replay Sessions + public struct ReplayStats: Codable { + /// The number of records produced during this view lifetime + public let recordsCount: Int64? + + /// The number of segments sent during this view lifetime + public let segmentsCount: Int64? + + /// The total size in bytes of the segments sent during this view lifetime + public let segmentsTotalRawSize: Int64? + + enum CodingKeys: String, CodingKey { + case recordsCount = "records_count" + case segmentsCount = "segments_count" + case segmentsTotalRawSize = "segments_total_raw_size" + } + } + /// Session-related internal properties public struct Session: Codable { - /// Session plan: 1 is the plan without replay, 2 is the plan with replay - public let plan: Plan + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? enum CodingKeys: String, CodingKey { case plan = "plan" } - /// Session plan: 1 is the plan without replay, 2 is the plan with replay + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) public enum Plan: Int, Codable { case plan1 = 1 case plan2 = 2 @@ -1580,11 +1733,78 @@ public struct RUMViewEvent: RUMDataModel { } } + /// Display properties + public struct Display: Codable { + /// Scroll properties + public let scroll: Scroll? + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case scroll = "scroll" + case viewport = "viewport" + } + + /// Scroll properties + public struct Scroll: Codable { + /// Distance between the top and the lowest point reached on this view (in pixels) + public let maxDepth: Double + + /// Page scroll height (total height) when the maximum scroll depth was reached for this view (in pixels) + public let maxDepthScrollHeight: Double + + /// Page scroll top (scrolled distance) when the maximum scroll depth was reached for this view (in pixels) + public let maxDepthScrollTop: Double + + /// Duration between the view start and the scroll event that reached the maximum scroll depth for this view (in nanoseconds) + public let maxDepthTime: Double + + enum CodingKeys: String, CodingKey { + case maxDepth = "max_depth" + case maxDepthScrollHeight = "max_depth_scroll_height" + case maxDepthScrollTop = "max_depth_scroll_top" + case maxDepthTime = "max_depth_time" + } + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + /// Feature flags properties public struct FeatureFlags: Codable { public internal(set) var featureFlagsInfo: [String: Encodable] } + /// Privacy properties + public struct Privacy: Codable { + /// The replay privacy level + public let replayLevel: ReplayLevel + + enum CodingKeys: String, CodingKey { + case replayLevel = "replay_level" + } + + /// The replay privacy level + public enum ReplayLevel: String, Codable { + case allow = "allow" + case mask = "mask" + case maskUserInput = "mask-user-input" + } + } + /// Session properties public struct Session: Codable { /// Whether this session has a replay @@ -1596,8 +1816,11 @@ public struct RUMViewEvent: RUMDataModel { /// Whether this session is currently active. Set to false to manually stop a session public let isActive: Bool? + /// Whether this session has been sampled for replay + public let sampledForReplay: Bool? + /// The precondition that led to the creation of the session - public let startReason: StartReason? + public let startPrecondition: StartPrecondition? /// Type of the session public let type: SessionType @@ -1606,16 +1829,17 @@ public struct RUMViewEvent: RUMDataModel { case hasReplay = "has_replay" case id = "id" case isActive = "is_active" - case startReason = "start_reason" + case sampledForReplay = "sampled_for_replay" + case startPrecondition = "start_precondition" case type = "type" } /// The precondition that led to the creation of the session - public enum StartReason: String, Codable { - case appStart = "app_start" + public enum StartPrecondition: String, Codable { + case appLaunch = "app_launch" case inactivityTimeout = "inactivity_timeout" case maxDuration = "max_duration" - case stopApi = "stop_api" + case explicitStop = "explicit_stop" case backgroundEvent = "background_event" } @@ -2447,6 +2671,9 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Whether initialization fails silently if the SDK is already initialized public let silentMultipleInit: Bool? + /// Whether the session replay start is handled manually + public var startSessionReplayRecordingManually: Bool? + /// The percentage of telemetry configuration events sent after being sampled by telemetry_sample_rate public let telemetryConfigurationSampleRate: Int64? @@ -2553,6 +2780,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case sessionReplaySampleRate = "session_replay_sample_rate" case sessionSampleRate = "session_sample_rate" case silentMultipleInit = "silent_multiple_init" + case startSessionReplayRecordingManually = "start_session_replay_recording_manually" case telemetryConfigurationSampleRate = "telemetry_configuration_sample_rate" case telemetrySampleRate = "telemetry_sample_rate" case traceSampleRate = "trace_sample_rate" @@ -2823,32 +3051,11 @@ public struct RUMDevice: Codable { } } -/// Display properties -public struct RUMDisplay: Codable { - /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. - public let viewport: Viewport? - - enum CodingKeys: String, CodingKey { - case viewport = "viewport" - } - - /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. - public struct Viewport: Codable { - /// Height of the viewport (in pixels) - public let height: Double - - /// Width of the viewport (in pixels) - public let width: Double - - enum CodingKeys: String, CodingKey { - case height = "height" - case width = "width" - } - } -} - /// Operating system properties public struct RUMOperatingSystem: Codable { + /// Operating system build number, e.g. 15D21 + public let build: String? + /// Operating system name, e.g. Android, iOS public let name: String @@ -2859,6 +3066,7 @@ public struct RUMOperatingSystem: Codable { public let versionMajor: String enum CodingKeys: String, CodingKey { + case build = "build" case name = "name" case version = "version" case versionMajor = "version_major" @@ -2974,4 +3182,4 @@ public enum RUMMethod: String, Codable { case patch = "PATCH" } -// Generated from https://github.com/DataDog/rum-events-format/tree/a45fbc913eb36f3bf0cc37aa1bdbee126104972b +// Generated from https://github.com/DataDog/rum-events-format/tree/1c5eaa897c065e5f790a5f8aaf6fc8782d706051 diff --git a/Sources/Datadog/RUM/DataModels/RUMDataModelsMapping.swift b/Sources/Datadog/RUM/DataModels/RUMDataModelsMapping.swift index 8cc79b5b66..e40fe2034c 100644 --- a/Sources/Datadog/RUM/DataModels/RUMDataModelsMapping.swift +++ b/Sources/Datadog/RUM/DataModels/RUMDataModelsMapping.swift @@ -57,3 +57,23 @@ internal extension RUMViewEvent.Source { } } } + +internal extension RUMViewEvent { + /// Metadata associated with the `RUMViewEvent`. + /// It may be used to filter out the `RUMViewEvent` from the batch. + struct Metadata: Codable { + let id: String + let documentVersion: Int64 + + private enum CodingKeys: String, CodingKey { + case id = "id" + case documentVersion = "document_version" + } + } + + /// Creates `Metadata` from the given `RUMViewEvent`. + /// - Returns: The `Metadata` for the given `RUMViewEvent`. + func metadata() -> Metadata { + return Metadata(id: view.id, documentVersion: dd.documentVersion) + } +} diff --git a/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift b/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift index 2272569653..1907baf083 100644 --- a/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift +++ b/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift @@ -373,6 +373,8 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { dd: .init( browserSdkVersion: nil, documentVersion: original.dd.documentVersion + 1, + pageStates: nil, + replayStats: nil, session: .init(plan: .plan1) ), application: original.application, @@ -383,6 +385,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { device: original.device, display: nil, os: original.os, + privacy: nil, service: original.service, session: original.session, source: original.source ?? .ios, @@ -446,6 +449,8 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { dd: .init( browserSdkVersion: nil, documentVersion: 1, + pageStates: nil, + replayStats: nil, session: .init(plan: .plan1) ), application: .init( @@ -465,12 +470,14 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { // before restarting the app after crash. To solve this, the OS information would have to be // persisted in `crashContext` the same way as we do for other dynamic information. os: .init(device: context.device), + privacy: nil, service: context.service, session: .init( hasReplay: hasReplay, id: sessionUUID.toRUMDataFormat, isActive: true, - startReason: nil, + sampledForReplay: nil, + startPrecondition: nil, type: CITestIntegration.active != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUM/Integrations/RUMContextAttributes.swift b/Sources/Datadog/RUM/Integrations/RUMContextAttributes.swift index 88dad103f9..728be34063 100644 --- a/Sources/Datadog/RUM/Integrations/RUMContextAttributes.swift +++ b/Sources/Datadog/RUM/Integrations/RUMContextAttributes.swift @@ -10,11 +10,11 @@ import Foundation internal enum RUMContextAttributes { internal enum IDs { /// The ID of RUM application (`String`). - internal static let applicationID = "application_id" + internal static let applicationID = "application.id" /// The ID of current RUM session (standard UUID `String`, lowercased). /// In case the session is rejected (not sampled), RUM context is set to empty (`[:]`) in core. - internal static let sessionID = "session_id" + internal static let sessionID = "session.id" /// The ID of current RUM view (standard UUID `String`, lowercased). internal static let viewID = "view.id" diff --git a/Sources/Datadog/RUM/Integrations/SessionReplayDependency.swift b/Sources/Datadog/RUM/Integrations/SessionReplayDependency.swift index c4250b93ee..dfccc80626 100644 --- a/Sources/Datadog/RUM/Integrations/SessionReplayDependency.swift +++ b/Sources/Datadog/RUM/Integrations/SessionReplayDependency.swift @@ -18,6 +18,9 @@ internal struct SessionReplayDependency { /// The key referencing a `Bool` value that indicates if replay is being recorded. static let hasReplay = "has_replay" + + /// The key referencing a `[String: Int64]` value that indicates number of records recorded for a given viewID. + static let recordsCountByViewID = "records_count_by_view_id" } // MARK: - Extracting SR context from `DatadogContext` @@ -31,10 +34,18 @@ extension DatadogContext { extension FeatureBaggage { /// The value indicating if replay is being performed by Session Replay. - var isReplayBeingRecorded: Bool { + var hasReplay: Bool { guard let hasReplay: Bool = self[SessionReplayDependency.hasReplay] else { return false } return hasReplay } + + /// The value of `[String: Int64]` that indicates number of records recorded for a given viewID. + var recordsCountByViewID: [String: Int64] { + guard let recordsCountByViewID: [String: Int64] = self[SessionReplayDependency.recordsCountByViewID] else { + return [:] + } + return recordsCountByViewID + } } diff --git a/Sources/Datadog/RUM/RUMEvent/RUMOperatingSystemInfo.swift b/Sources/Datadog/RUM/RUMEvent/RUMOperatingSystemInfo.swift index 59bed57c28..0d9503f8de 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMOperatingSystemInfo.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMOperatingSystemInfo.swift @@ -15,5 +15,6 @@ extension RUMOperatingSystem { self.name = device.osName self.version = device.osVersion self.versionMajor = device.osVersion.split(separator: ".").first.map { String($0) } ?? device.osVersion + self.build = nil } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index e7938762d3..fd1e28b934 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -117,7 +117,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { parent: self, startTime: context.sdkInitDate, dependencies: dependencies, - isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded + hasReplay: context.srBaggage?.hasReplay ) sessionScopes.append(initialSession) @@ -149,7 +149,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { parent: self, startTime: command.time, dependencies: dependencies, - isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, resumingViewScope: resumingViewScope ) lastActiveView = nil diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift index 4583be071b..c01787e241 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift @@ -200,7 +200,7 @@ internal class RUMResourceScope: RUMScope { ), service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, type: dependencies.ciTest != nil ? .ciTest : .user ), @@ -260,7 +260,7 @@ internal class RUMResourceScope: RUMScope { os: .init(context: context), service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, type: dependencies.ciTest != nil ? .ciTest : .user ), diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMScopeDependencies.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMScopeDependencies.swift index f36095f6c3..facbc8ad4d 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMScopeDependencies.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMScopeDependencies.swift @@ -27,8 +27,6 @@ internal struct RUMScopeDependencies { let rumUUIDGenerator: RUMUUIDGenerator /// Integration with CIApp tests. It contains the CIApp test context when active. let ciTest: RUMCITest? - /// Produces `RUMViewUpdatesThrottlerType` for each started RUM view scope. - let viewUpdatesThrottlerFactory: () -> RUMViewUpdatesThrottlerType let vitalsReaders: VitalsReaders? let onSessionStart: RUMSessionListener? @@ -57,7 +55,6 @@ internal extension RUMScopeDependencies { ), rumUUIDGenerator: rumFeature.configuration.uuidGenerator, ciTest: CITestIntegration.active?.rumCITest, - viewUpdatesThrottlerFactory: { RUMViewUpdatesThrottler() }, vitalsReaders: rumFeature.configuration.vitalsFrequency.map { .init( frequency: $0, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index b7a4fcb4bd..1b2812b404 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -63,7 +63,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { parent: RUMContextProvider, startTime: Date, dependencies: RUMScopeDependencies, - isReplayBeingRecorded: Bool?, + hasReplay: Bool?, resumingViewScope: RUMViewScope? = nil ) { self.parent = parent @@ -79,7 +79,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { sessionUUID: sessionUUID.rawValue, isInitialSession: isInitialSession, hasTrackedAnyView: false, - didStartWithReplay: isReplayBeingRecorded + didStartWithReplay: hasReplay ) if let viewScope = resumingViewScope, @@ -115,7 +115,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { parent: expiredSession.parent, startTime: startTime, dependencies: expiredSession.dependencies, - isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded + hasReplay: context.srBaggage?.hasReplay ) // Transfer active Views by creating new `RUMViewScopes` for their identity objects: diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift index 221b3e551f..3ec078de00 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift @@ -163,7 +163,7 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider { os: .init(context: context), service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, type: dependencies.ciTest != nil ? .ciTest : .user ), diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 864cde834b..e9a6f64b85 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -89,9 +89,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { private let vitalInfoSampler: VitalInfoSampler? - /// Samples view update events, so we can minimize the number of events in payload. - private let viewUpdatesThrottler: RUMViewUpdatesThrottlerType - private var viewPerformanceMetrics: [PerformanceMetric: VitalInfo] = [:] init( @@ -126,7 +123,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { frequency: $0.frequency ) } - self.viewUpdatesThrottler = dependencies.viewUpdatesThrottlerFactory() } // MARK: - RUMContextProvider @@ -394,7 +390,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { os: .init(context: context), service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, type: dependencies.ciTest != nil ? .ciTest : .user ), @@ -441,6 +437,12 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { dd: .init( browserSdkVersion: nil, documentVersion: version.toInt64, + pageStates: nil, + replayStats: .init( + recordsCount: context.srBaggage?.recordsCountByViewID[viewUUID.toRUMDataFormat], + segmentsCount: nil, + segmentsTotalRawSize: nil + ), session: .init(plan: .plan1) ), application: .init(id: self.context.rumApplicationID), @@ -452,12 +454,14 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { display: nil, featureFlags: .init(featureFlagsInfo: featureFlags), os: .init(context: context), + privacy: nil, service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, isActive: self.context.isSessionActive, - startReason: nil, + sampledForReplay: nil, + startPrecondition: nil, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -508,11 +512,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { ) if let event = dependencies.eventBuilder.build(from: viewEvent) { - if viewUpdatesThrottler.accept(event: event) { - writer.write(value: event) - } else { // if event was dropped by sampler - version -= 1 - } + writer.write(value: event, metadata: event.metadata()) // Update `CrashContext` with recent RUM view (no matter sampling - we want to always // have recent information if process is interrupted by crash): @@ -563,7 +563,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { os: .init(context: context), service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, type: dependencies.ciTest != nil ? .ciTest : .user ), @@ -614,7 +614,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { os: .init(context: context), service: context.service, session: .init( - hasReplay: context.srBaggage?.isReplayBeingRecorded, + hasReplay: context.srBaggage?.hasReplay, id: self.context.sessionID.toRUMDataFormat, type: dependencies.ciTest != nil ? .ciTest : .user ), diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/Utils/RUMViewUpdatesThrottler.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/Utils/RUMViewUpdatesThrottler.swift deleted file mode 100644 index ab70e5ac47..0000000000 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/Utils/RUMViewUpdatesThrottler.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import Foundation - -internal protocol RUMViewUpdatesThrottlerType { - func accept(event: RUMViewEvent) -> Bool -} - -/// An utility suppressing the number of view updates sent for a single view. -/// -/// It uses time-based heuristic for view updates throttling: -/// - it suppresses updates which happen more frequent than `viewUpdateThreshold`, -/// - it always samples (accepts) first and last update for the view. -internal final class RUMViewUpdatesThrottler: RUMViewUpdatesThrottlerType { - struct Constants { - /// Default suppression interval, in seconds. - static let defaultViewUpdateThreshold: TimeInterval = 30.0 - } - - /// Suppression interval, in nanoseconds. - private let viewUpdateThresholdInNs: Int64 - /// The `timeSpent` (in ns) from the last accepted view event. - private var lastSampledTimeSpentInNs: Int64? = nil - - init(viewUpdateThreshold: TimeInterval = Constants.defaultViewUpdateThreshold) { - self.viewUpdateThresholdInNs = viewUpdateThreshold.toInt64Nanoseconds - } - - /// Based on the `viewUpdateThresholdInNs` and `viewUpdate.timeSpent`, it decides if an event should be "sampled" or not. - /// - Returns: `true` if event should be sent to Datadog and `false` if it should be dropped. - func accept(event: RUMViewEvent) -> Bool { - var sample: Bool - - if let lastTimeSpent = lastSampledTimeSpentInNs { - sample = (event.view.timeSpent - lastTimeSpent) >= viewUpdateThresholdInNs - } else { - sample = true // always accept the first event in a view - } - - sample = sample || event.view.isActive == false // always accept the last event in a view - - sample = sample || event.view.crash.map { $0.count > 0 } ?? false - - if sample { - lastSampledTimeSpentInNs = event.view.timeSpent - } - - return sample - } -} diff --git a/Sources/Datadog/RUM/RUMV2Configuration.swift b/Sources/Datadog/RUM/RUMV2Configuration.swift index 9a40728fe1..0aa20e4517 100644 --- a/Sources/Datadog/RUM/RUMV2Configuration.swift +++ b/Sources/Datadog/RUM/RUMV2Configuration.swift @@ -13,7 +13,10 @@ import Foundation internal func createRUMConfiguration(configuration: FeaturesConfiguration.RUM) -> DatadogFeatureConfiguration { return DatadogFeatureConfiguration( name: "rum", - requestBuilder: RUMRequestBuilder(intake: configuration.uploadURL), + requestBuilder: RUMRequestBuilder( + intake: configuration.uploadURL, + eventsFilter: RUMViewEventsFilter() + ), messageReceiver: CombinedFeatureMessageReceiver( ErrorMessageReceiver(), WebViewEventReceiver( @@ -30,6 +33,50 @@ internal func createRUMConfiguration(configuration: FeaturesConfiguration.RUM) - ) } +internal struct RUMViewEventsFilter { + let decoder: JSONDecoder + + init(decoder: JSONDecoder = JSONDecoder()) { + self.decoder = decoder + } + + func filter(events: [Event]) -> [Event] { + var seen = Set() + var skipped: [String: [Int64]] = [:] + + // reversed is O(1) and no copy because it is view on the original array + let filtered = events.reversed().compactMap { event in + guard let metadata = event.metadata else { + // If there is no metadata, we can't filter it. + return event + } + + guard let viewMetadata = try? decoder.decode(RUMViewEvent.Metadata.self, from: metadata) else { + // If we can't decode the metadata, we can't filter it. + return event + } + + guard seen.contains(viewMetadata.id) == false else { + // If we've already seen this view, we can skip this + if skipped[viewMetadata.id] == nil { + skipped[viewMetadata.id] = [] + } + skipped[viewMetadata.id]?.append(viewMetadata.documentVersion) + return nil + } + + seen.insert(viewMetadata.id) + return event + } + + for (id, versions) in skipped { + DD.logger.debug("Skipping RUMViewEvent with id: \(id) and versions: \(versions.reversed().map(String.init).joined(separator: ", "))") + } + + return filtered.reversed() + } +} + /// The RUM URL Request Builder for formatting and configuring the `URLRequest` /// to upload RUM data. internal struct RUMRequestBuilder: FeatureRequestBuilder { @@ -39,7 +86,9 @@ internal struct RUMRequestBuilder: FeatureRequestBuilder { /// The RUM request body format. let format = DataFormat(prefix: "", suffix: "", separator: "\n") - func request(for events: [Data], with context: DatadogContext) -> URLRequest { + let eventsFilter: RUMViewEventsFilter + + func request(for events: [Event], with context: DatadogContext) -> URLRequest { var tags = [ "service:\(context.service)", "version:\(context.version)", @@ -71,7 +120,8 @@ internal struct RUMRequestBuilder: FeatureRequestBuilder { ] ) - let data = format.format(events) + let filteredEvents = eventsFilter.filter(events: events) + let data = format.format(filteredEvents.map { $0.data }) return builder.uploadRequest(with: data) } } diff --git a/Sources/Datadog/TracerConfiguration.swift b/Sources/Datadog/TracerConfiguration.swift index 2a5fab9098..674fdef14b 100644 --- a/Sources/Datadog/TracerConfiguration.swift +++ b/Sources/Datadog/TracerConfiguration.swift @@ -27,7 +27,7 @@ extension Tracer { /// - Parameter enabled: `true` by default public var bundleWithRUM: Bool - /// TRhe sampling rate for Traces, as a Float between 0 and 100. + /// The sampling rate for Traces, as a Float between 0 and 100. Default is 100. public var samplingRate: Float /// Initializes the Datadog Tracer configuration. diff --git a/Sources/Datadog/Tracing/TracingV2Configuration.swift b/Sources/Datadog/Tracing/TracingV2Configuration.swift index be27569e2e..ca18890d98 100644 --- a/Sources/Datadog/Tracing/TracingV2Configuration.swift +++ b/Sources/Datadog/Tracing/TracingV2Configuration.swift @@ -18,6 +18,21 @@ internal func createTracingConfiguration(intake: URL) -> DatadogFeatureConfigura ) } +internal func mapInternalTags(_ originalTag: String) -> String { + switch originalTag { + case "application.id": + return "_dd.application.id" + case "session.id": + return "_dd.session.id" + case "view.id": + return "_dd.view.id" + case "user_action.id": + return "_dd.action.id" + default: + return originalTag + } +} + /// The Tracing URL Request Builder for formatting and configuring the `URLRequest` /// to upload traces data. internal struct TracingRequestBuilder: FeatureRequestBuilder { @@ -27,7 +42,7 @@ internal struct TracingRequestBuilder: FeatureRequestBuilder { /// The tracing request body format. let format = DataFormat(prefix: "", suffix: "", separator: "\n") - func request(for events: [Data], with context: DatadogContext) -> URLRequest { + func request(for events: [Event], with context: DatadogContext) -> URLRequest { let builder = URLRequestBuilder( url: intake, queryItems: [], @@ -45,7 +60,7 @@ internal struct TracingRequestBuilder: FeatureRequestBuilder { ] ) - let data = format.format(events) + let data = format.format(events.map { $0.data }) return builder.uploadRequest(with: data) } } @@ -72,8 +87,11 @@ internal struct TracingMessageReceiver: FeatureMessageReceiver { /// /// - Parameter context: The updated core context. private func update(context: DatadogContext) -> Bool { - if let attributes: [String: String] = context.featuresAttributes["rum"]?.ids { - rum.attributes = attributes + if let attributes: [String: String?] = context.featuresAttributes["rum"]?.ids { + let attributes = attributes.compactMapValues { $0 } + let mappedAttribues = Dictionary(uniqueKeysWithValues: attributes.map { key, value in (mapInternalTags(key), value) }) + + rum.attributes = mappedAttribues return true } return false diff --git a/Sources/Datadog/Versioning.swift b/Sources/Datadog/Versioning.swift index d43cebee9f..3ac7b0b136 100644 --- a/Sources/Datadog/Versioning.swift +++ b/Sources/Datadog/Versioning.swift @@ -1,3 +1,3 @@ // GENERATED FILE: Do not edit directly -internal let __sdkVersion = "1.21.0" +internal let __sdkVersion = "1.22.0" diff --git a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift index 1b41e5bee6..d43b1355ab 100644 --- a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift +++ b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift @@ -52,8 +52,8 @@ public class DDRUMActionEvent: NSObject { root.swiftModel.device != nil ? DDRUMActionEventRUMDevice(root: root) : nil } - @objc public var display: DDRUMActionEventRUMDisplay? { - root.swiftModel.display != nil ? DDRUMActionEventRUMDisplay(root: root) : nil + @objc public var display: DDRUMActionEventDisplay? { + root.swiftModel.display != nil ? DDRUMActionEventDisplay(root: root) : nil } @objc public var os: DDRUMActionEventRUMOperatingSystem? { @@ -188,20 +188,23 @@ public class DDRUMActionEventDDSession: NSObject { @objc public enum DDRUMActionEventDDSessionPlan: Int { - internal init(swift: RUMActionEvent.DD.Session.Plan) { + internal init(swift: RUMActionEvent.DD.Session.Plan?) { switch swift { - case .plan1: self = .plan1 - case .plan2: self = .plan2 + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 } } - internal var toSwift: RUMActionEvent.DD.Session.Plan { + internal var toSwift: RUMActionEvent.DD.Session.Plan? { switch self { + case .none: return nil case .plan1: return .plan1 case .plan2: return .plan2 } } + case none case plan1 case plan2 } @@ -600,20 +603,20 @@ public enum DDRUMActionEventRUMDeviceRUMDeviceType: Int { } @objc -public class DDRUMActionEventRUMDisplay: NSObject { +public class DDRUMActionEventDisplay: NSObject { internal let root: DDRUMActionEvent internal init(root: DDRUMActionEvent) { self.root = root } - @objc public var viewport: DDRUMActionEventRUMDisplayViewport? { - root.swiftModel.display!.viewport != nil ? DDRUMActionEventRUMDisplayViewport(root: root) : nil + @objc public var viewport: DDRUMActionEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMActionEventDisplayViewport(root: root) : nil } } @objc -public class DDRUMActionEventRUMDisplayViewport: NSObject { +public class DDRUMActionEventDisplayViewport: NSObject { internal let root: DDRUMActionEvent internal init(root: DDRUMActionEvent) { @@ -637,6 +640,10 @@ public class DDRUMActionEventRUMOperatingSystem: NSObject { self.root = root } + @objc public var build: String? { + root.swiftModel.os!.build + } + @objc public var name: String { root.swiftModel.os!.name } @@ -848,8 +855,8 @@ public class DDRUMErrorEvent: NSObject { root.swiftModel.device != nil ? DDRUMErrorEventRUMDevice(root: root) : nil } - @objc public var display: DDRUMErrorEventRUMDisplay? { - root.swiftModel.display != nil ? DDRUMErrorEventRUMDisplay(root: root) : nil + @objc public var display: DDRUMErrorEventDisplay? { + root.swiftModel.display != nil ? DDRUMErrorEventDisplay(root: root) : nil } @objc public var error: DDRUMErrorEventError { @@ -933,20 +940,23 @@ public class DDRUMErrorEventDDSession: NSObject { @objc public enum DDRUMErrorEventDDSessionPlan: Int { - internal init(swift: RUMErrorEvent.DD.Session.Plan) { + internal init(swift: RUMErrorEvent.DD.Session.Plan?) { switch swift { - case .plan1: self = .plan1 - case .plan2: self = .plan2 + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 } } - internal var toSwift: RUMErrorEvent.DD.Session.Plan { + internal var toSwift: RUMErrorEvent.DD.Session.Plan? { switch self { + case .none: return nil case .plan1: return .plan1 case .plan2: return .plan2 } } + case none case plan1 case plan2 } @@ -1193,20 +1203,20 @@ public enum DDRUMErrorEventRUMDeviceRUMDeviceType: Int { } @objc -public class DDRUMErrorEventRUMDisplay: NSObject { +public class DDRUMErrorEventDisplay: NSObject { internal let root: DDRUMErrorEvent internal init(root: DDRUMErrorEvent) { self.root = root } - @objc public var viewport: DDRUMErrorEventRUMDisplayViewport? { - root.swiftModel.display!.viewport != nil ? DDRUMErrorEventRUMDisplayViewport(root: root) : nil + @objc public var viewport: DDRUMErrorEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMErrorEventDisplayViewport(root: root) : nil } } @objc -public class DDRUMErrorEventRUMDisplayViewport: NSObject { +public class DDRUMErrorEventDisplayViewport: NSObject { internal let root: DDRUMErrorEvent internal init(root: DDRUMErrorEvent) { @@ -1235,6 +1245,11 @@ public class DDRUMErrorEventError: NSObject { get { root.swiftModel.error.causes?.map { DDRUMErrorEventErrorCauses(swiftModel: $0) } } } + @objc public var fingerprint: String? { + set { root.swiftModel.error.fingerprint = newValue } + get { root.swiftModel.error.fingerprint } + } + @objc public var handling: DDRUMErrorEventErrorHandling { .init(swift: root.swiftModel.error.handling) } @@ -1599,6 +1614,10 @@ public class DDRUMErrorEventRUMOperatingSystem: NSObject { self.root = root } + @objc public var build: String? { + root.swiftModel.os!.build + } + @objc public var name: String { root.swiftModel.os!.name } @@ -1810,8 +1829,8 @@ public class DDRUMLongTaskEvent: NSObject { root.swiftModel.device != nil ? DDRUMLongTaskEventRUMDevice(root: root) : nil } - @objc public var display: DDRUMLongTaskEventRUMDisplay? { - root.swiftModel.display != nil ? DDRUMLongTaskEventRUMDisplay(root: root) : nil + @objc public var display: DDRUMLongTaskEventDisplay? { + root.swiftModel.display != nil ? DDRUMLongTaskEventDisplay(root: root) : nil } @objc public var longTask: DDRUMLongTaskEventLongTask { @@ -1895,20 +1914,23 @@ public class DDRUMLongTaskEventDDSession: NSObject { @objc public enum DDRUMLongTaskEventDDSessionPlan: Int { - internal init(swift: RUMLongTaskEvent.DD.Session.Plan) { + internal init(swift: RUMLongTaskEvent.DD.Session.Plan?) { switch swift { - case .plan1: self = .plan1 - case .plan2: self = .plan2 + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 } } - internal var toSwift: RUMLongTaskEvent.DD.Session.Plan { + internal var toSwift: RUMLongTaskEvent.DD.Session.Plan? { switch self { + case .none: return nil case .plan1: return .plan1 case .plan2: return .plan2 } } + case none case plan1 case plan2 } @@ -2155,20 +2177,20 @@ public enum DDRUMLongTaskEventRUMDeviceRUMDeviceType: Int { } @objc -public class DDRUMLongTaskEventRUMDisplay: NSObject { +public class DDRUMLongTaskEventDisplay: NSObject { internal let root: DDRUMLongTaskEvent internal init(root: DDRUMLongTaskEvent) { self.root = root } - @objc public var viewport: DDRUMLongTaskEventRUMDisplayViewport? { - root.swiftModel.display!.viewport != nil ? DDRUMLongTaskEventRUMDisplayViewport(root: root) : nil + @objc public var viewport: DDRUMLongTaskEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMLongTaskEventDisplayViewport(root: root) : nil } } @objc -public class DDRUMLongTaskEventRUMDisplayViewport: NSObject { +public class DDRUMLongTaskEventDisplayViewport: NSObject { internal let root: DDRUMLongTaskEvent internal init(root: DDRUMLongTaskEvent) { @@ -2213,6 +2235,10 @@ public class DDRUMLongTaskEventRUMOperatingSystem: NSObject { self.root = root } + @objc public var build: String? { + root.swiftModel.os!.build + } + @objc public var name: String { root.swiftModel.os!.name } @@ -2420,8 +2446,8 @@ public class DDRUMResourceEvent: NSObject { root.swiftModel.device != nil ? DDRUMResourceEventRUMDevice(root: root) : nil } - @objc public var display: DDRUMResourceEventRUMDisplay? { - root.swiftModel.display != nil ? DDRUMResourceEventRUMDisplay(root: root) : nil + @objc public var display: DDRUMResourceEventDisplay? { + root.swiftModel.display != nil ? DDRUMResourceEventDisplay(root: root) : nil } @objc public var os: DDRUMResourceEventRUMOperatingSystem? { @@ -2517,20 +2543,23 @@ public class DDRUMResourceEventDDSession: NSObject { @objc public enum DDRUMResourceEventDDSessionPlan: Int { - internal init(swift: RUMResourceEvent.DD.Session.Plan) { + internal init(swift: RUMResourceEvent.DD.Session.Plan?) { switch swift { - case .plan1: self = .plan1 - case .plan2: self = .plan2 + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 } } - internal var toSwift: RUMResourceEvent.DD.Session.Plan { + internal var toSwift: RUMResourceEvent.DD.Session.Plan? { switch self { + case .none: return nil case .plan1: return .plan1 case .plan2: return .plan2 } } + case none case plan1 case plan2 } @@ -2777,20 +2806,20 @@ public enum DDRUMResourceEventRUMDeviceRUMDeviceType: Int { } @objc -public class DDRUMResourceEventRUMDisplay: NSObject { +public class DDRUMResourceEventDisplay: NSObject { internal let root: DDRUMResourceEvent internal init(root: DDRUMResourceEvent) { self.root = root } - @objc public var viewport: DDRUMResourceEventRUMDisplayViewport? { - root.swiftModel.display!.viewport != nil ? DDRUMResourceEventRUMDisplayViewport(root: root) : nil + @objc public var viewport: DDRUMResourceEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMResourceEventDisplayViewport(root: root) : nil } } @objc -public class DDRUMResourceEventRUMDisplayViewport: NSObject { +public class DDRUMResourceEventDisplayViewport: NSObject { internal let root: DDRUMResourceEvent internal init(root: DDRUMResourceEvent) { @@ -2814,6 +2843,10 @@ public class DDRUMResourceEventRUMOperatingSystem: NSObject { self.root = root } + @objc public var build: String? { + root.swiftModel.os!.build + } + @objc public var name: String { root.swiftModel.os!.name } @@ -2847,8 +2880,8 @@ public class DDRUMResourceEventResource: NSObject { root.swiftModel.resource.download != nil ? DDRUMResourceEventResourceDownload(root: root) : nil } - @objc public var duration: NSNumber { - root.swiftModel.resource.duration as NSNumber + @objc public var duration: NSNumber? { + root.swiftModel.resource.duration as NSNumber? } @objc public var firstByte: DDRUMResourceEventResourceFirstByte? { @@ -3347,8 +3380,8 @@ public class DDRUMViewEvent: NSObject { root.swiftModel.device != nil ? DDRUMViewEventRUMDevice(root: root) : nil } - @objc public var display: DDRUMViewEventRUMDisplay? { - root.swiftModel.display != nil ? DDRUMViewEventRUMDisplay(root: root) : nil + @objc public var display: DDRUMViewEventDisplay? { + root.swiftModel.display != nil ? DDRUMViewEventDisplay(root: root) : nil } @objc public var featureFlags: DDRUMViewEventFeatureFlags? { @@ -3359,6 +3392,10 @@ public class DDRUMViewEvent: NSObject { root.swiftModel.os != nil ? DDRUMViewEventRUMOperatingSystem(root: root) : nil } + @objc public var privacy: DDRUMViewEventPrivacy? { + root.swiftModel.privacy != nil ? DDRUMViewEventPrivacy(root: root) : nil + } + @objc public var service: String? { root.swiftModel.service } @@ -3412,11 +3449,87 @@ public class DDRUMViewEventDD: NSObject { root.swiftModel.dd.formatVersion as NSNumber } + @objc public var pageStates: [DDRUMViewEventDDPageStates]? { + root.swiftModel.dd.pageStates?.map { DDRUMViewEventDDPageStates(swiftModel: $0) } + } + + @objc public var replayStats: DDRUMViewEventDDReplayStats? { + root.swiftModel.dd.replayStats != nil ? DDRUMViewEventDDReplayStats(root: root) : nil + } + @objc public var session: DDRUMViewEventDDSession? { root.swiftModel.dd.session != nil ? DDRUMViewEventDDSession(root: root) : nil } } +@objc +public class DDRUMViewEventDDPageStates: NSObject { + internal var swiftModel: RUMViewEvent.DD.PageStates + internal var root: DDRUMViewEventDDPageStates { self } + + internal init(swiftModel: RUMViewEvent.DD.PageStates) { + self.swiftModel = swiftModel + } + + @objc public var start: NSNumber { + root.swiftModel.start as NSNumber + } + + @objc public var state: DDRUMViewEventDDPageStatesState { + .init(swift: root.swiftModel.state) + } +} + +@objc +public enum DDRUMViewEventDDPageStatesState: Int { + internal init(swift: RUMViewEvent.DD.PageStates.State) { + switch swift { + case .active: self = .active + case .passive: self = .passive + case .hidden: self = .hidden + case .frozen: self = .frozen + case .terminated: self = .terminated + } + } + + internal var toSwift: RUMViewEvent.DD.PageStates.State { + switch self { + case .active: return .active + case .passive: return .passive + case .hidden: return .hidden + case .frozen: return .frozen + case .terminated: return .terminated + } + } + + case active + case passive + case hidden + case frozen + case terminated +} + +@objc +public class DDRUMViewEventDDReplayStats: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var recordsCount: NSNumber? { + root.swiftModel.dd.replayStats!.recordsCount as NSNumber? + } + + @objc public var segmentsCount: NSNumber? { + root.swiftModel.dd.replayStats!.segmentsCount as NSNumber? + } + + @objc public var segmentsTotalRawSize: NSNumber? { + root.swiftModel.dd.replayStats!.segmentsTotalRawSize as NSNumber? + } +} + @objc public class DDRUMViewEventDDSession: NSObject { internal let root: DDRUMViewEvent @@ -3432,20 +3545,23 @@ public class DDRUMViewEventDDSession: NSObject { @objc public enum DDRUMViewEventDDSessionPlan: Int { - internal init(swift: RUMViewEvent.DD.Session.Plan) { + internal init(swift: RUMViewEvent.DD.Session.Plan?) { switch swift { - case .plan1: self = .plan1 - case .plan2: self = .plan2 + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 } } - internal var toSwift: RUMViewEvent.DD.Session.Plan { + internal var toSwift: RUMViewEvent.DD.Session.Plan? { switch self { + case .none: return nil case .plan1: return .plan1 case .plan2: return .plan2 } } + case none case plan1 case plan2 } @@ -3656,20 +3772,49 @@ public enum DDRUMViewEventRUMDeviceRUMDeviceType: Int { } @objc -public class DDRUMViewEventRUMDisplay: NSObject { +public class DDRUMViewEventDisplay: NSObject { internal let root: DDRUMViewEvent internal init(root: DDRUMViewEvent) { self.root = root } - @objc public var viewport: DDRUMViewEventRUMDisplayViewport? { - root.swiftModel.display!.viewport != nil ? DDRUMViewEventRUMDisplayViewport(root: root) : nil + @objc public var scroll: DDRUMViewEventDisplayScroll? { + root.swiftModel.display!.scroll != nil ? DDRUMViewEventDisplayScroll(root: root) : nil + } + + @objc public var viewport: DDRUMViewEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMViewEventDisplayViewport(root: root) : nil } } @objc -public class DDRUMViewEventRUMDisplayViewport: NSObject { +public class DDRUMViewEventDisplayScroll: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var maxDepth: NSNumber { + root.swiftModel.display!.scroll!.maxDepth as NSNumber + } + + @objc public var maxDepthScrollHeight: NSNumber { + root.swiftModel.display!.scroll!.maxDepthScrollHeight as NSNumber + } + + @objc public var maxDepthScrollTop: NSNumber { + root.swiftModel.display!.scroll!.maxDepthScrollTop as NSNumber + } + + @objc public var maxDepthTime: NSNumber { + root.swiftModel.display!.scroll!.maxDepthTime as NSNumber + } +} + +@objc +public class DDRUMViewEventDisplayViewport: NSObject { internal let root: DDRUMViewEvent internal init(root: DDRUMViewEvent) { @@ -3706,6 +3851,10 @@ public class DDRUMViewEventRUMOperatingSystem: NSObject { self.root = root } + @objc public var build: String? { + root.swiftModel.os!.build + } + @objc public var name: String { root.swiftModel.os!.name } @@ -3719,6 +3868,42 @@ public class DDRUMViewEventRUMOperatingSystem: NSObject { } } +@objc +public class DDRUMViewEventPrivacy: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var replayLevel: DDRUMViewEventPrivacyReplayLevel { + .init(swift: root.swiftModel.privacy!.replayLevel) + } +} + +@objc +public enum DDRUMViewEventPrivacyReplayLevel: Int { + internal init(swift: RUMViewEvent.Privacy.ReplayLevel) { + switch swift { + case .allow: self = .allow + case .mask: self = .mask + case .maskUserInput: self = .maskUserInput + } + } + + internal var toSwift: RUMViewEvent.Privacy.ReplayLevel { + switch self { + case .allow: return .allow + case .mask: return .mask + case .maskUserInput: return .maskUserInput + } + } + + case allow + case mask + case maskUserInput +} + @objc public class DDRUMViewEventSession: NSObject { internal let root: DDRUMViewEvent @@ -3739,8 +3924,12 @@ public class DDRUMViewEventSession: NSObject { root.swiftModel.session.isActive as NSNumber? } - @objc public var startReason: DDRUMViewEventSessionStartReason { - .init(swift: root.swiftModel.session.startReason) + @objc public var sampledForReplay: NSNumber? { + root.swiftModel.session.sampledForReplay as NSNumber? + } + + @objc public var startPrecondition: DDRUMViewEventSessionStartPrecondition { + .init(swift: root.swiftModel.session.startPrecondition) } @objc public var type: DDRUMViewEventSessionSessionType { @@ -3749,34 +3938,34 @@ public class DDRUMViewEventSession: NSObject { } @objc -public enum DDRUMViewEventSessionStartReason: Int { - internal init(swift: RUMViewEvent.Session.StartReason?) { +public enum DDRUMViewEventSessionStartPrecondition: Int { + internal init(swift: RUMViewEvent.Session.StartPrecondition?) { switch swift { case nil: self = .none - case .appStart?: self = .appStart + case .appLaunch?: self = .appLaunch case .inactivityTimeout?: self = .inactivityTimeout case .maxDuration?: self = .maxDuration - case .stopApi?: self = .stopApi + case .explicitStop?: self = .explicitStop case .backgroundEvent?: self = .backgroundEvent } } - internal var toSwift: RUMViewEvent.Session.StartReason? { + internal var toSwift: RUMViewEvent.Session.StartPrecondition? { switch self { case .none: return nil - case .appStart: return .appStart + case .appLaunch: return .appLaunch case .inactivityTimeout: return .inactivityTimeout case .maxDuration: return .maxDuration - case .stopApi: return .stopApi + case .explicitStop: return .explicitStop case .backgroundEvent: return .backgroundEvent } } case none - case appStart + case appLaunch case inactivityTimeout case maxDuration - case stopApi + case explicitStop case backgroundEvent } @@ -4879,6 +5068,11 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.silentMultipleInit as NSNumber? } + @objc public var startSessionReplayRecordingManually: NSNumber? { + set { root.swiftModel.telemetry.configuration.startSessionReplayRecordingManually = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.startSessionReplayRecordingManually as NSNumber? } + } + @objc public var telemetryConfigurationSampleRate: NSNumber? { root.swiftModel.telemetry.configuration.telemetryConfigurationSampleRate as NSNumber? } @@ -5131,4 +5325,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/a45fbc913eb36f3bf0cc37aa1bdbee126104972b +// Generated from https://github.com/DataDog/rum-events-format/tree/1c5eaa897c065e5f790a5f8aaf6fc8782d706051 diff --git a/TestUtilities/Mocks/FileWriterMock.swift b/TestUtilities/Mocks/FileWriterMock.swift index 71704b94b5..11af7908d8 100644 --- a/TestUtilities/Mocks/FileWriterMock.swift +++ b/TestUtilities/Mocks/FileWriterMock.swift @@ -16,7 +16,7 @@ public class FileWriterMock: Writer { /// Adds an `Encodable` event to the events stack. /// /// - Parameter value: The event value to record. - public func write(value: T) where T: Encodable { + public func write(value: T, metadata: M) { events.append(value) } diff --git a/TestUtilities/Mocks/PassthroughCoreMock.swift b/TestUtilities/Mocks/PassthroughCoreMock.swift index 874f692943..9ff31413cb 100644 --- a/TestUtilities/Mocks/PassthroughCoreMock.swift +++ b/TestUtilities/Mocks/PassthroughCoreMock.swift @@ -101,6 +101,14 @@ public final class PassthroughCoreMock: DatadogV1CoreProtocol, FeatureScope { context.featuresAttributes[feature] = attributes() } + public func update(feature: String, attributes: @escaping () -> FeatureBaggage) { + if context.featuresAttributes[feature] != nil { + context.featuresAttributes[feature]?.merge(with: attributes()) + } else { + context.featuresAttributes[feature] = attributes() + } + } + public func send(message: FeatureMessage, sender: DatadogCoreProtocol, else fallback: () -> Void) { if !messageReceiver.receive(message: message, from: sender) { fallback() diff --git a/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift b/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift index fb24618f6e..9f106352d5 100644 --- a/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift +++ b/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift @@ -13,14 +13,14 @@ extension PerformancePreset { struct FeatureRequestBuilderMock: FeatureRequestBuilder { let dataFormat = DataFormat(prefix: "", suffix: "", separator: "\n") - func request(for events: [Data], with context: DatadogContext) -> URLRequest { + func request(for events: [Event], with context: DatadogContext) -> URLRequest { let builder = URLRequestBuilder( url: .mockAny(), queryItems: [.ddtags(tags: ["foo:bar"])], headers: [] ) - let data = dataFormat.format(events) + let data = dataFormat.format(events.map { $0.data }) return builder.uploadRequest(with: data) } } diff --git a/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift index 97d600a81f..35f34d1b44 100644 --- a/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift @@ -35,7 +35,7 @@ class DataUploaderBenchmarkTests: BenchmarkTests { measure(metrics: [XCTMemoryMetric()]) { // in each, 10 requests are done: try? (0..<10).forEach { _ in - let events = [Data(repeating: 0x41, count: 10 * 1_024 * 1_024)] + let events = [Event(data: Data(repeating: 0x41, count: 10 * 1_024 * 1_024))] _ = try dataUploader.upload(events: events, context: context) } // After all, the baseline asserts `0kB` or less grow in Physical Memory. diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift index 5abe02ec2a..5fc6501114 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift @@ -79,8 +79,9 @@ class RUMManualInstrumentationScenarioTests: IntegrationTests, RUMCommonAsserts XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) - XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration, 100_000_000 - 1) // ~0.1s - XCTAssertLessThan(view1.resourceEvents[0].resource.duration, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) + XCTAssertNotNil(view1.resourceEvents[0].resource.duration) // ~0.1s + XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration!, 100_000_000 - 1) // ~0.1s + XCTAssertLessThan(view1.resourceEvents[0].resource.duration!, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) XCTAssertEqual(view1.errorEvents[0].error.type, "NSURLErrorDomain - -1011") XCTAssertEqual(view1.errorEvents[0].error.message, "Bad response.") XCTAssertEqual( diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMResourcesScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMResourcesScenarioTests.swift index 488c68c8e3..b39966be65 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMResourcesScenarioTests.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMResourcesScenarioTests.swift @@ -130,7 +130,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { "RUM Resource should be send for `firstPartyGETResourceURL`" ) XCTAssertEqual(firstPartyResource1.resource.method, .get) - XCTAssertGreaterThan(firstPartyResource1.resource.duration, 0) + XCTAssertNotNil(firstPartyResource1.resource.duration) + XCTAssertGreaterThan(firstPartyResource1.resource.duration!, 0) XCTAssertNil(firstPartyResource1.dd.traceId, "`firstPartyGETResourceURL` should not be traced") XCTAssertNil(firstPartyResource1.dd.spanId, "`firstPartyGETResourceURL` should not be traced") @@ -141,7 +142,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { "RUM Resource should be send for `firstPartyPOSTResourceURL`" ) XCTAssertEqual(firstPartyResource2.resource.method, .post) - XCTAssertGreaterThan(firstPartyResource2.resource.duration, 0) + XCTAssertNotNil(firstPartyResource2.resource.duration) + XCTAssertGreaterThan(firstPartyResource2.resource.duration!, 0) XCTAssertEqual( firstPartyResource2.dd.traceId, firstPartyPOSTRequestTraceID, @@ -172,7 +174,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { "RUM Resource should be send for `thirdPartyGETResourceURL`" ) XCTAssertEqual(thirdPartyResource1.resource.method, .get) - XCTAssertGreaterThan(thirdPartyResource1.resource.duration, 0) + XCTAssertNotNil(thirdPartyResource1.resource.duration) + XCTAssertGreaterThan(thirdPartyResource1.resource.duration!, 0) XCTAssertNil(thirdPartyResource1.dd.traceId, "3rd party RUM Resources should not be traced") XCTAssertNil(thirdPartyResource1.dd.spanId, "3rd party RUM Resources should not be traced") XCTAssertNil(thirdPartyResource1.dd.rulePsr, "Not traced resource should not send sample rate") @@ -182,7 +185,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts { "RUM Resource should be send for `thirdPartyPOSTResourceURL`" ) XCTAssertEqual(thirdPartyResource2.resource.method, .post) - XCTAssertGreaterThan(thirdPartyResource2.resource.duration, 0) + XCTAssertNotNil(thirdPartyResource2.resource.duration) + XCTAssertGreaterThan(thirdPartyResource2.resource.duration!, 0) XCTAssertNil(thirdPartyResource2.dd.traceId, "3rd party RUM Resources should not be traced") XCTAssertNil(thirdPartyResource2.dd.spanId, "3rd party RUM Resources should not be traced") XCTAssertNil(thirdPartyResource2.dd.rulePsr, "Not traced resource should not send sample rate") diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift index ff83cffeb3..0b06a10800 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift @@ -100,8 +100,9 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) - XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration, 100_000_000 - 1) // ~0.1s - XCTAssertLessThan(view1.resourceEvents[0].resource.duration, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) + XCTAssertNotNil(view1.resourceEvents[0].resource.duration) + XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration!, 100_000_000 - 1) // ~0.1s + XCTAssertLessThan(view1.resourceEvents[0].resource.duration!, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) RUMSessionMatcher.assertViewWasEventuallyInactive(view1) let view2 = normalSession.viewVisits[1] @@ -123,8 +124,9 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) - XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration, 100_000_000 - 1) // ~0.1s - XCTAssertLessThan(view1.resourceEvents[0].resource.duration, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) + XCTAssertNotNil(view1.resourceEvents[0].resource.duration) + XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration!, 100_000_000 - 1) // ~0.1s + XCTAssertLessThan(view1.resourceEvents[0].resource.duration!, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) RUMSessionMatcher.assertViewWasEventuallyInactive(view1) let view2 = interruptedSession.viewVisits[1] diff --git a/Tests/DatadogTests/Datadog/Core/FeatureTests.swift b/Tests/DatadogTests/Datadog/Core/FeatureTests.swift index fdc991f9d7..385e8ef02e 100644 --- a/Tests/DatadogTests/Datadog/Core/FeatureTests.swift +++ b/Tests/DatadogTests/Datadog/Core/FeatureTests.swift @@ -84,7 +84,7 @@ class FeatureStorageTests: XCTestCase { storage.setIgnoreFilesAgeWhenReading(to: true) let batch = try XCTUnwrap(storage.reader.readNextBatch()) - XCTAssertEqual(batch.events.map { $0.utf8String }, [#"{"event.consent":"granted"}"#]) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"granted"}"#]) storage.reader.markBatchAsRead(batch) XCTAssertNil(storage.reader.readNextBatch(), "There must be no other batches") @@ -103,11 +103,11 @@ class FeatureStorageTests: XCTestCase { storage.setIgnoreFilesAgeWhenReading(to: true) var batch = try XCTUnwrap(storage.reader.readNextBatch()) - XCTAssertEqual(batch.events.map { $0.utf8String }, [#"{"event.consent":"granted"}"#]) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"granted"}"#]) storage.reader.markBatchAsRead(batch) batch = try XCTUnwrap(storage.reader.readNextBatch()) - XCTAssertEqual(batch.events.map { $0.utf8String }, [#"{"event.consent":"pending"}"#]) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"pending"}"#]) storage.reader.markBatchAsRead(batch) XCTAssertNil(storage.reader.readNextBatch(), "There must be no other batches") @@ -126,7 +126,7 @@ class FeatureStorageTests: XCTestCase { storage.setIgnoreFilesAgeWhenReading(to: true) let batch = try XCTUnwrap(storage.reader.readNextBatch()) - XCTAssertEqual(batch.events.map { $0.utf8String }, [#"{"event.consent":"granted"}"#]) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"granted"}"#]) storage.reader.markBatchAsRead(batch) XCTAssertNil(storage.reader.readNextBatch(), "There must be no other batches") diff --git a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift index f761f9a789..f09166669e 100644 --- a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift +++ b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift @@ -140,8 +140,22 @@ class PerformancePresetTests: XCTestCase { func testPresetsUpdate() { let preset = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .mockRandom(), bundleType: .mockRandom()) - let updatedPreset = preset.updated(with: PerformancePresetOverride(maxFileSize: .mockRandom(), maxObjectSize: .mockRandom())) + let updatedPreset = preset.updated( + with: PerformancePresetOverride( + maxFileSize: .mockRandom(), + maxObjectSize: .mockRandom(), + meanFileAge: .mockRandom(), + minUploadDelay: .mockRandom() + ) + ) + XCTAssertNotEqual(preset.maxFileSize, updatedPreset.maxFileSize) XCTAssertNotEqual(preset.maxObjectSize, updatedPreset.maxObjectSize) + XCTAssertNotEqual(preset.maxFileAgeForWrite, updatedPreset.maxFileAgeForWrite) + XCTAssertNotEqual(preset.minFileAgeForRead, updatedPreset.minFileAgeForRead) + XCTAssertNotEqual(preset.initialUploadDelay, updatedPreset.initialUploadDelay) + XCTAssertNotEqual(preset.minUploadDelay, updatedPreset.minUploadDelay) + XCTAssertNotEqual(preset.maxUploadDelay, updatedPreset.maxUploadDelay) + XCTAssertEqual(0.1, updatedPreset.uploadDelayChangeRate) } } diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/DataBlockTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/DataBlockTests.swift index e09b65ca5e..2a6b20e98a 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/DataBlockTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/DataBlockTests.swift @@ -8,6 +8,20 @@ import XCTest @testable import Datadog class DataBlockTests: XCTestCase { + func testSerializeEventBlock() throws { + XCTAssertEqual( + try DataBlock(type: .event, data: Data([0xFF])).serialize(), + Data([0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF]) + ) + } + + func testSerializeEventMetadataBlock() throws { + XCTAssertEqual( + try DataBlock(type: .eventMetadata, data: Data([0xFF])).serialize(), + Data([0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF]) + ) + } + func testSerializeDataBlock() throws { XCTAssertEqual( try DataBlock(type: .event, data: Data([0xFF])).serialize(), @@ -33,6 +47,22 @@ class DataBlockTests: XCTestCase { XCTAssertEqual(data.prefix(7), Data([0x00, 0x00, 0x80, 0x96, 0x98, 0x00, 0xFF])) } + func testDataBlockReader_withEventDataBlock() throws { + let data = Data([0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF]) + let reader = DataBlockReader(data: data) + let block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, Data([0xFF])) + } + + func testDataBlockReader_withEventMetadataBlock() throws { + let data = Data([0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF]) + let reader = DataBlockReader(data: data) + let block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, Data([0xFF])) + } + func testDataBlockReader_withSingleBlock() throws { let data = Data([0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF]) let reader = DataBlockReader(data: data) diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/EventGeneratorTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/EventGeneratorTests.swift new file mode 100644 index 0000000000..451150543a --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Persistence/EventGeneratorTests.swift @@ -0,0 +1,85 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +final class EventGeneratorTests: XCTestCase { + func testEmpty() throws { + let dataBlocks = [DataBlock]() + + let sut = EventGenerator(dataBlocks: dataBlocks) + let events = sut.map { $0 } + XCTAssertEqual(events.count, 0) + } + + func testWithoutEvent() throws { + let dataBlocks = [DataBlock(type: .eventMetadata, data: Data([0x01]))] + + let sut = EventGenerator(dataBlocks: dataBlocks) + let events = sut.map { $0 } + XCTAssertEqual(events.count, 0) + } + + func testEventWithoutMetadata() throws { + let dataBlocks = [DataBlock(type: .event, data: Data([0x01]))] + + let sut = EventGenerator(dataBlocks: dataBlocks) + let events = sut.map { $0 } + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events[0].data, Data([0x01])) + XCTAssertNil(events[0].metadata) + } + + func testEventWithMetadata() throws { + let dataBlocks = [ + DataBlock(type: .eventMetadata, data: Data([0x02])), + DataBlock(type: .event, data: Data([0x01])) + ] + + let sut = EventGenerator(dataBlocks: dataBlocks) + let events = sut.map { $0 } + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events[0].metadata, Data([0x02])) + XCTAssertEqual(events[0].data, Data([0x01])) + } + + func testEventWithCurruptedMetadata() throws { + let dataBlocks = [ + DataBlock(type: .eventMetadata, data: Data([0x03])), // skipped from reading + DataBlock(type: .eventMetadata, data: Data([0x02])), + DataBlock(type: .event, data: Data([0x01])) + ] + + let sut = EventGenerator(dataBlocks: dataBlocks) + let events = sut.map { $0 } + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events[0].metadata, Data([0x02])) + XCTAssertEqual(events[0].data, Data([0x01])) + } + + func testEvents() { + let dataBlocks = [ + DataBlock(type: .eventMetadata, data: Data([0x02])), + DataBlock(type: .event, data: Data([0x01])), + DataBlock(type: .event, data: Data([0x03])), + DataBlock(type: .event, data: Data([0x05])) + ] + + let sut = EventGenerator(dataBlocks: dataBlocks) + let events = sut.map { $0 } + XCTAssertEqual(events.count, 3) + + XCTAssertEqual(events[0].data, Data([0x01])) + XCTAssertEqual(events[0].metadata, Data([0x02])) + + XCTAssertEqual(events[1].data, Data([0x03])) + XCTAssertNil(events[1].metadata) + + XCTAssertEqual(events[2].data, Data([0x05])) + XCTAssertNil(events[2].metadata) + } +} diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/Reading/FileReaderTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/Reading/FileReaderTests.swift index 23f5d29234..dd0b92b787 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/Reading/FileReaderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/Reading/FileReaderTests.swift @@ -26,19 +26,37 @@ class FileReaderTests: XCTestCase { dateProvider: SystemDateProvider() ) ) + let dataBlocks = [ + DataBlock(type: .eventMetadata, data: "EFGH".utf8Data), + DataBlock(type: .event, data: "ABCD".utf8Data) + ] + let data = try dataBlocks + .map { try $0.serialize() } + .reduce(.init(), +) _ = try temporaryDirectory .createFile(named: Date.mockAny().toFileName) - .append(data: DataBlock(type: .event, data: "ABCD".utf8Data).serialize()) + .append(data: data) XCTAssertEqual(try temporaryDirectory.files().count, 1) let batch = reader.readNextBatch() - XCTAssertEqual(batch?.events, ["ABCD".utf8Data]) + + let expected = [ + Event(data: "ABCD".utf8Data, metadata: "EFGH".utf8Data) + ] + XCTAssertEqual(batch?.events, expected) } func testItReadsSingleEncryptedBatch() throws { // Given - let data = try Array(repeating: "foo".utf8Data, count: 3) - .map { try DataBlock(type: .event, data: $0).serialize() } + let dataBlocks = [ + DataBlock(type: .eventMetadata, data: "foo".utf8Data), + DataBlock(type: .event, data: "foo".utf8Data), + DataBlock(type: .event, data: "foo".utf8Data), + DataBlock(type: .eventMetadata, data: "foo".utf8Data), + DataBlock(type: .event, data: "foo".utf8Data) + ] + let data = try dataBlocks + .map { Data(try $0.serialize()) } .reduce(.init(), +) _ = try temporaryDirectory @@ -60,7 +78,12 @@ class FileReaderTests: XCTestCase { let batch = reader.readNextBatch() // Then - XCTAssertEqual(batch?.events, ["bar","bar","bar"].map { $0.utf8Data }) + let expected = [ + Event(data: "bar".utf8Data, metadata: "bar".utf8Data), + Event(data: "bar".utf8Data, metadata: nil), + Event(data: "bar".utf8Data, metadata: "bar".utf8Data) + ] + XCTAssertEqual(batch?.events, expected) } func testItMarksBatchesAsRead() throws { @@ -73,25 +96,33 @@ class FileReaderTests: XCTestCase { ) ) let file1 = try temporaryDirectory.createFile(named: dateProvider.now.toFileName) + try file1.append(data: DataBlock(type: .eventMetadata, data: "2".utf8Data).serialize()) try file1.append(data: DataBlock(type: .event, data: "1".utf8Data).serialize()) let file2 = try temporaryDirectory.createFile(named: dateProvider.now.toFileName) try file2.append(data: DataBlock(type: .event, data: "2".utf8Data).serialize()) let file3 = try temporaryDirectory.createFile(named: dateProvider.now.toFileName) + try file3.append(data: DataBlock(type: .eventMetadata, data: "4".utf8Data).serialize()) try file3.append(data: DataBlock(type: .event, data: "3".utf8Data).serialize()) + let expected = [ + Event(data: "1".utf8Data, metadata: "2".utf8Data), + Event(data: "2".utf8Data, metadata: nil), + Event(data: "3".utf8Data, metadata: "4".utf8Data) + ] + var batch: Batch batch = try reader.readNextBatch().unwrapOrThrow() - XCTAssertEqual(batch.events.first, "1".utf8Data) + XCTAssertEqual(batch.events.first, expected[0]) reader.markBatchAsRead(batch) batch = try reader.readNextBatch().unwrapOrThrow() - XCTAssertEqual(batch.events.first, "2".utf8Data) + XCTAssertEqual(batch.events.first, expected[1]) reader.markBatchAsRead(batch) batch = try reader.readNextBatch().unwrapOrThrow() - XCTAssertEqual(batch.events.first, "3".utf8Data) + XCTAssertEqual(batch.events.first, expected[2]) reader.markBatchAsRead(batch) XCTAssertNil(reader.readNextBatch()) diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/Writing/FileWriterTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/Writing/FileWriterTests.swift index ae07db0a0b..fda0d37ed8 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/Writing/FileWriterTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/Writing/FileWriterTests.swift @@ -19,6 +19,82 @@ class FileWriterTests: XCTestCase { super.tearDown() } + func testItWritesDataWithMetadataToSingleFileInTLVFormat() throws { + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider() + ), + encryption: nil, + forceNewFile: false + ) + + writer.write(value: ["key1": "value1"], metadata: ["meta1": "metaValue1"]) + writer.write(value: ["key2": "value2"]) // skipped metadata here + writer.write(value: ["key3": "value3"], metadata: ["meta3": "metaValue3"]) + + XCTAssertEqual(try temporaryDirectory.files().count, 1) + let stream = try temporaryDirectory.files()[0].stream() + + let reader = DataBlockReader(input: stream) + var block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"{"meta1":"metaValue1"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key1":"value1"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key2":"value2"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"{"meta3":"metaValue3"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key3":"value3"}"#.utf8Data) + } + + func testItWritesEncryptedDataWithMetadataToSingleFileInTLVFormat() throws { + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider() + ), + encryption: DataEncryptionMock( + encrypt: { data in + "encrypted".utf8Data + data + "encrypted".utf8Data + } + ), + forceNewFile: false + ) + + writer.write(value: ["key1": "value1"], metadata: ["meta1": "metaValue1"]) + writer.write(value: ["key2": "value2"]) // skipped metadata here + writer.write(value: ["key3": "value3"], metadata: ["meta3": "metaValue3"]) + + XCTAssertEqual(try temporaryDirectory.files().count, 1) + let stream = try temporaryDirectory.files()[0].stream() + + let reader = DataBlockReader(input: stream) + var block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"encrypted{"meta1":"metaValue1"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"encrypted{"key1":"value1"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"encrypted{"key2":"value2"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"encrypted{"meta3":"metaValue3"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"encrypted{"key3":"value3"}encrypted"#.utf8Data) + } + func testItWritesDataToSingleFileInTLVFormat() throws { let writer = FileWriter( orchestrator: FilesOrchestrator( @@ -192,13 +268,20 @@ class FileWriterTests: XCTestCase { ioInterruptionQueue.async { try? file?.makeReadWrite() } } - struct Foo: Codable { + struct Foo: Codable, Equatable { var foo = "bar" } + struct Metadata: Codable, Equatable { + var meta = "data" + } + + let foo = Foo() + let metadata = Metadata() + // Write 300 of `Foo`s and interrupt writes randomly (0..<300).forEach { _ in - writer.write(value: Foo()) + writer.write(value: foo, metadata: metadata) randomlyInterruptIO(for: try? temporaryDirectory.files().first) } @@ -211,15 +294,24 @@ class FileWriterTests: XCTestCase { // Assert that data written is not malformed let jsonDecoder = JSONDecoder() - let events = try blocks.map { try jsonDecoder.decode(Foo.self, from: $0.data) } + let eventGenerator = EventGenerator(dataBlocks: blocks) + let events = eventGenerator.map { $0 } // Assert that some (including all) `Foo`s were written XCTAssertGreaterThan(events.count, 0) XCTAssertLessThanOrEqual(events.count, 300) + for event in events { + let actualFoo = try jsonDecoder.decode(Foo.self, from: event.data) + XCTAssertEqual(actualFoo, foo) + + XCTAssertNotNil(event.metadata) + let actualMetadata = try jsonDecoder.decode(Metadata.self, from: event.metadata!) + XCTAssertEqual(actualMetadata, metadata) + } } func testItWritesEncryptedDataToSingleFile() throws { - // Given + // Given let writer = FileWriter( orchestrator: FilesOrchestrator( directory: temporaryDirectory, @@ -233,21 +325,32 @@ class FileWriterTests: XCTestCase { ) // When - writer.write(value: ["key1": "value1"]) + writer.write(value: ["key1": "value1"], metadata: ["meta1": "metaValue1"]) writer.write(value: ["key2": "value3"]) - writer.write(value: ["key3": "value3"]) + writer.write(value: ["key3": "value3"], metadata: ["meta3": "metaValue3"]) // Then XCTAssertEqual(try temporaryDirectory.files().count, 1) let stream = try temporaryDirectory.files()[0].stream() let reader = DataBlockReader(input: stream) + var block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, "foo".utf8Data) + + block = try reader.next() XCTAssertEqual(block?.type, .event) XCTAssertEqual(block?.data, "foo".utf8Data) + block = try reader.next() XCTAssertEqual(block?.type, .event) XCTAssertEqual(block?.data, "foo".utf8Data) + + block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, "foo".utf8Data) + block = try reader.next() XCTAssertEqual(block?.type, .event) XCTAssertEqual(block?.data, "foo".utf8Data) diff --git a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift index e12f11f042..5bf872fa6e 100644 --- a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -72,7 +72,7 @@ class DatadogCoreTests: XCTestCase { let uploadedEvents = requestBuilderSpy.requestParameters .flatMap { $0.events } - .map { $0.utf8String } + .map { $0.data.utf8String } XCTAssertEqual(uploadedEvents, [#"{"event":"granted"}"#], "Only `.granted` events should be uploaded") XCTAssertEqual(requestBuilderSpy.requestParameters.count, 1, "It should send only one request") @@ -121,7 +121,7 @@ class DatadogCoreTests: XCTestCase { let uploadedEvents = requestBuilderSpy.requestParameters .flatMap { $0.events } - .map { $0.utf8String } + .map { $0.data.utf8String } XCTAssertEqual( uploadedEvents, @@ -175,7 +175,7 @@ class DatadogCoreTests: XCTestCase { let uploadedEvents = requestBuilderSpy.requestParameters .flatMap { $0.events } - .map { $0.utf8String } + .map { $0.data.utf8String } XCTAssertEqual( uploadedEvents, @@ -214,11 +214,49 @@ class DatadogCoreTests: XCTestCase { try core.register( feature: FeatureMock( name: name, - performanceOverride: PerformancePresetOverride(maxFileSize: 123, maxObjectSize: 456) + performanceOverride: PerformancePresetOverride( + maxFileSize: 123, + maxObjectSize: 456, + meanFileAge: 100, + minUploadDelay: nil + ) ) ) feature = core.v2Features.values.first XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, 456) XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, 123) + XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileAgeForWrite, 95) + XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.minFileAgeForRead, 105) + } + + func testItUpdatesTheFeatureBaggage() throws { + // Given + let contextProvider: DatadogContextProvider = .mockAny() + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + userInfoProvider: .mockAny(), + performance: .mockRandom(), + httpClient: .mockAny(), + encryption: nil, + contextProvider: contextProvider, + applicationVersion: .mockAny() + ) + defer { core.flushAndTearDown() } + try core.register(feature: FeatureMock(name: "mock")) + + // When + core.update(feature: "mock") { + return ["foo": "bar"] + } + core.update(feature: "mock") { + return ["bizz": "bazz"] + } + + // Then + let context = contextProvider.read() + XCTAssertEqual(context.featuresAttributes["mock"]?.foo, "bar") + XCTAssertEqual(context.featuresAttributes["mock"]?.bizz, "bazz") } } diff --git a/Tests/DatadogTests/Datadog/DatadogInternal/Context/FeatureBaggageTests.swift b/Tests/DatadogTests/Datadog/DatadogInternal/Context/FeatureBaggageTests.swift index 8b49944781..9e8505411f 100644 --- a/Tests/DatadogTests/Datadog/DatadogInternal/Context/FeatureBaggageTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogInternal/Context/FeatureBaggageTests.swift @@ -47,4 +47,14 @@ class FeatureBaggageTests: XCTestCase { attributes.int = 2 XCTAssertEqual(attributes.int, 2) } + + func testMerge() { + var attributes = attributes + attributes.merge(with: ["string": "test2"]) + + XCTAssertEqual(attributes.int, 1) + XCTAssertEqual(attributes.double, 2.0) + XCTAssertEqual(attributes.string, "test2") + XCTAssertEqual(attributes.enum, EnumAttribute.test) + } } diff --git a/Tests/DatadogTests/Datadog/DatadogInternal/Upload/DataFormatTests.swift b/Tests/DatadogTests/Datadog/DatadogInternal/Upload/DataFormatTests.swift new file mode 100644 index 0000000000..e08396115b --- /dev/null +++ b/Tests/DatadogTests/Datadog/DatadogInternal/Upload/DataFormatTests.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +final class DataFormatTests: XCTestCase { + func testFormat() throws { + let format = DataFormat(prefix: "prefix", suffix: "suffix", separator: "\n") + let events = [ + "abc".data(using: .utf8)!, + "def".data(using: .utf8)!, + "ghi".data(using: .utf8)! + ] + let formatted = format.format(events) + let actual = String(data: formatted, encoding: .utf8)! + let expected = + """ + prefixabc + def + ghisuffix + """ + XCTAssertEqual(actual, expected) + } +} diff --git a/Tests/DatadogTests/Datadog/LoggerTests.swift b/Tests/DatadogTests/Datadog/LoggerTests.swift index 9cec620c43..86779ff4c7 100644 --- a/Tests/DatadogTests/Datadog/LoggerTests.swift +++ b/Tests/DatadogTests/Datadog/LoggerTests.swift @@ -562,7 +562,7 @@ class LoggerTests: XCTestCase { logMatchers.forEach { $0.assertValue( - forKeyPath: RUMContextAttributes.IDs.sessionID, + forKeyPath: "application_id", isTypeOf: String.self ) } @@ -594,17 +594,17 @@ class LoggerTests: XCTestCase { logMatchers.forEach { $0.assertValue( - forKeyPath: RUMContextAttributes.IDs.applicationID, + forKeyPath: "application_id", equals: rum.configuration.applicationID ) $0.assertValue( - forKeyPath: RUMContextAttributes.IDs.sessionID, + forKeyPath: "session_id", isTypeOf: String.self ) $0.assertValue( - forKeyPath: RUMContextAttributes.IDs.viewID, + forKeyPath: "view.id", isTypeOf: String.self ) } @@ -631,7 +631,7 @@ class LoggerTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rum - ).replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: SystemDateProvider() ) Global.rum.startView(viewController: mockView) @@ -675,7 +675,7 @@ class LoggerTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rum - ).replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: SystemDateProvider() ) Global.rum.startView(viewController: mockView) @@ -725,7 +725,7 @@ class LoggerTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rum - ).replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: SystemDateProvider() ) Global.rum.startView(viewController: mockView) @@ -771,7 +771,7 @@ class LoggerTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rum - ).replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: SystemDateProvider() ) Global.rum.startView(viewController: mockView) diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index d8947f61ec..000eedce3b 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -659,7 +659,7 @@ struct DataUploaderMock: DataUploaderType { var onUpload: (() throws -> Void)? = nil - func upload(events: [Data], context: DatadogContext) throws -> DataUploadStatus { + func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus { try onUpload?() return uploadStatus } diff --git a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index 4cc526dc0d..dd5a48acd5 100644 --- a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -87,6 +87,10 @@ internal class DatadogCoreProxy: DatadogCoreProtocol { core.set(feature: feature, attributes: attributes) } + func update(feature: String, attributes: @escaping () -> FeatureBaggage) { + core.update(feature: feature, attributes: attributes) + } + func send(message: FeatureMessage, sender: DatadogCoreProtocol, else fallback: @escaping () -> Void) { core.send(message: message, sender: self, else: fallback) } @@ -149,8 +153,8 @@ private class FeatureScopeInterceptor { let actualWriter: Writer unowned var interception: FeatureScopeInterceptor? - func write(value: T) where T: Encodable { - actualWriter.write(value: value) + func write(value: T, metadata: M) { + actualWriter.write(value: value, metadata: metadata) let event = value let data = try! InterceptingWriter.jsonEncoder.encode(value) diff --git a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/UploadMock.swift b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/UploadMock.swift index 2e340fdc9b..cec3814e9e 100644 --- a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/UploadMock.swift +++ b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/UploadMock.swift @@ -26,18 +26,18 @@ internal struct FeatureRequestBuilderMock: FeatureRequestBuilder { self.format = format } - func request(for events: [Data], with context: DatadogContext) throws -> URLRequest { + func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { let builder = URLRequestBuilder(url: url, queryItems: queryItems, headers: headers) - let data = format.format(events) + let data = format.format(events.map { $0.data }) return builder.uploadRequest(with: data) } } internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { /// Records parameters passed to `requet(for:with:)` - var requestParameters: [(events: [Data], context: DatadogContext)] = [] + var requestParameters: [(events: [Event], context: DatadogContext)] = [] - func request(for events: [Data], with context: DatadogContext) throws -> URLRequest { + func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { requestParameters.append((events: events, context: context)) return .mockAny() } @@ -46,7 +46,7 @@ internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { internal struct FailingRequestBuilderMock: FeatureRequestBuilder { let error: Error - func request(for events: [Data], with context: DatadogContext) throws -> URLRequest { + func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { throw error } } diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift index 3a7ffc3136..08be91cc36 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift @@ -85,6 +85,7 @@ extension RUMDevice.RUMDeviceType: RandomMockable { extension RUMOperatingSystem: RandomMockable { public static func mockRandom() -> RUMOperatingSystem { return .init( + build: nil, name: .mockRandom(length: 5), version: .mockRandom(among: .decimalDigits, length: 2), versionMajor: .mockRandom(among: .decimalDigits, length: 1) @@ -107,6 +108,8 @@ extension RUMViewEvent: RandomMockable { dd: .init( browserSdkVersion: nil, documentVersion: .mockRandom(), + pageStates: nil, + replayStats: nil, session: .init(plan: .plan1) ), application: .init(id: .mockRandom()), @@ -117,12 +120,14 @@ extension RUMViewEvent: RandomMockable { device: .mockRandom(), display: nil, os: .mockRandom(), + privacy: nil, service: .mockRandom(), session: .init( hasReplay: nil, id: .mockRandom(), isActive: true, - startReason: .appStart, + sampledForReplay: nil, + startPrecondition: .appLaunch, type: .user ), source: .ios, diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index b781746d76..5ae978c36c 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -676,12 +676,6 @@ extension RUMSessionState: AnyMockable, RandomMockable { // MARK: - RUMScope Mocks -internal struct NoOpRUMViewUpdatesThrottler: RUMViewUpdatesThrottlerType { - func accept(event: RUMViewEvent) -> Bool { - return true // always send view update - } -} - func mockNoOpSessionListener() -> RUMSessionListener { return { _, _ in } } @@ -701,7 +695,6 @@ extension RUMScopeDependencies { eventBuilder: RUMEventBuilder = RUMEventBuilder(eventsMapper: .mockNoOp()), rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), ciTest: RUMCITest? = nil, - viewUpdatesThrottlerFactory: @escaping () -> RUMViewUpdatesThrottlerType = { NoOpRUMViewUpdatesThrottler() }, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListener() ) -> RUMScopeDependencies { @@ -715,7 +708,6 @@ extension RUMScopeDependencies { eventBuilder: eventBuilder, rumUUIDGenerator: rumUUIDGenerator, ciTest: ciTest, - viewUpdatesThrottlerFactory: viewUpdatesThrottlerFactory, vitalsReaders: vitalsReaders, onSessionStart: onSessionStart ) @@ -731,7 +723,6 @@ extension RUMScopeDependencies { eventBuilder: RUMEventBuilder? = nil, rumUUIDGenerator: RUMUUIDGenerator? = nil, ciTest: RUMCITest? = nil, - viewUpdatesThrottlerFactory: (() -> RUMViewUpdatesThrottlerType)? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: RUMSessionListener? = nil ) -> RUMScopeDependencies { @@ -745,7 +736,6 @@ extension RUMScopeDependencies { eventBuilder: eventBuilder ?? self.eventBuilder, rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator, ciTest: ciTest ?? self.ciTest, - viewUpdatesThrottlerFactory: viewUpdatesThrottlerFactory ?? self.viewUpdatesThrottlerFactory, vitalsReaders: vitalsReaders ?? self.vitalsReaders, onSessionStart: onSessionStart ?? self.onSessionStart ) @@ -769,14 +759,14 @@ extension RUMSessionScope { parent: RUMContextProvider = RUMContextProviderMock(), startTime: Date = .mockAny(), dependencies: RUMScopeDependencies = .mockAny(), - isReplayBeingRecorded: Bool? = .mockAny() + hasReplay: Bool? = .mockAny() ) -> RUMSessionScope { return RUMSessionScope( isInitialSession: isInitialSession, parent: parent, startTime: startTime, dependencies: dependencies, - isReplayBeingRecorded: isReplayBeingRecorded + hasReplay: hasReplay ) } } @@ -1039,10 +1029,11 @@ class ContinuousVitalReaderMock: ContinuousVitalReader { // MARK: - Dependency on Session Replay extension Dictionary where Key == String, Value == FeatureBaggage { - static func mockSessionReplayAttributes(hasReplay: Bool?) -> Self { + static func mockSessionReplayAttributes(hasReplay: Bool?, recordsCountByViewID: [String: Int64]? = nil) -> Self { return [ SessionReplayDependency.srBaggageKey: [ - SessionReplayDependency.hasReplay: hasReplay + SessionReplayDependency.hasReplay: hasReplay, + SessionReplayDependency.recordsCountByViewID: recordsCountByViewID ] ] } diff --git a/Tests/DatadogTests/Datadog/RUM/Integrations/SessionReplayDependencyTests.swift b/Tests/DatadogTests/Datadog/RUM/Integrations/SessionReplayDependencyTests.swift index 4d53024cbd..9231b8d074 100644 --- a/Tests/DatadogTests/Datadog/RUM/Integrations/SessionReplayDependencyTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/Integrations/SessionReplayDependencyTests.swift @@ -10,14 +10,16 @@ import XCTest class SessionReplayDependencyTests: XCTestCase { func testWhenSessionReplayIsConfigured_itReadsReplayBeingRecorded() { let hasReplay: Bool = .random() + let recordsCountByViewID: [String: Int64] = [.mockRandom(): .mockRandom()] // When let context: DatadogContext = .mockWith( - featuresAttributes: .mockSessionReplayAttributes(hasReplay: hasReplay) + featuresAttributes: .mockSessionReplayAttributes(hasReplay: hasReplay, recordsCountByViewID: recordsCountByViewID) ) // Then - XCTAssertEqual(context.srBaggage?.isReplayBeingRecorded, hasReplay) + XCTAssertEqual(context.srBaggage?.hasReplay, hasReplay) + XCTAssertEqual(context.srBaggage?.recordsCountByViewID, recordsCountByViewID) } func testWhenSessionReplayIsNotConfigured_itReadsNoSRBaggage() { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMFeatureTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMFeatureTests.swift index 6e2c4c9cdb..2442142475 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMFeatureTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMFeatureTests.swift @@ -210,22 +210,21 @@ class RUMFeatureTests: XCTestCase { core.register(feature: feature) let writer = feature.storage.writer(for: .granted, forceNewBatch: false) - writer.write(value: RUMDataModelMock(attribute: "1st event")) - writer.write(value: RUMDataModelMock(attribute: "2nd event")) - writer.write(value: RUMDataModelMock(attribute: "3rd event")) + writer.write(value: RUMDataModelMock(attribute: "1st event"), metadata: RUMViewEvent.Metadata(id: "1", documentVersion: 1)) + writer.write(value: RUMDataModelMock(attribute: "2nd event"), metadata: RUMViewEvent.Metadata(id: "2", documentVersion: 1)) + writer.write(value: RUMDataModelMock(attribute: "3rd event"), metadata: RUMViewEvent.Metadata(id: "1", documentVersion: 2)) let payload = try XCTUnwrap(server.waitAndReturnRequests(count: 1)[0].httpBody) // Expected payload format: + // event1JSON is skipped in favor of event3JSON which is same event with higher document revision // ``` - // event1JSON // event2JSON // event3JSON // ``` let eventMatchers = try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(payload) - XCTAssertEqual((try eventMatchers[0].model() as RUMDataModelMock).attribute, "1st event") - XCTAssertEqual((try eventMatchers[1].model() as RUMDataModelMock).attribute, "2nd event") - XCTAssertEqual((try eventMatchers[2].model() as RUMDataModelMock).attribute, "3rd event") + XCTAssertEqual((try eventMatchers[0].model() as RUMDataModelMock).attribute, "2nd event") + XCTAssertEqual((try eventMatchers[1].model() as RUMDataModelMock).attribute, "3rd event") } } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMInternalProxyTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMInternalProxyTests.swift index 86d166ed64..3f24e57bcb 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMInternalProxyTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMInternalProxyTests.swift @@ -32,7 +32,7 @@ class RUMInternalProxyTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rumFeature - ).replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: rumFeature.configuration.dateProvider ) } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 6a637f6209..de1fd6c1b7 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -317,7 +317,7 @@ class RUMSessionScopeTests: XCTestCase { core: core, sessionSampler: .mockRandom() // no matter if sampled or not ), - isReplayBeingRecorded: randomIsReplayBeingRecorded + hasReplay: randomIsReplayBeingRecorded ) // Then @@ -352,7 +352,7 @@ class RUMSessionScopeTests: XCTestCase { parent: parent, startTime: sessionStartTime, dependencies: .mockWith(core: core), - isReplayBeingRecorded: randomIsReplayBeingRecorded + hasReplay: randomIsReplayBeingRecorded ) // When diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index 939726ee23..1853086286 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -183,10 +183,6 @@ class RUMViewScopeTests: XCTestCase { } func testWhenInitialViewReceivesAnyCommand_itSendsViewUpdateEvent() throws { - let hasReplay: Bool = .mockRandom() - var context = self.context - context.featuresAttributes = .mockSessionReplayAttributes(hasReplay: hasReplay) - let currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( isInitialView: true, @@ -200,6 +196,14 @@ class RUMViewScopeTests: XCTestCase { startTime: currentTime, serverTimeOffset: .zero ) + + let hasReplay: Bool = .mockRandom() + var context = self.context + context.featuresAttributes = .mockSessionReplayAttributes( + hasReplay: hasReplay, + recordsCountByViewID: [scope.viewUUID.toRUMDataFormat: 1] + ) + _ = scope.process( command: RUMCommandMock(time: currentTime), context: context, @@ -227,6 +231,7 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.service, "test-service") XCTAssertEqual(event.device?.name, "device-name") XCTAssertEqual(event.os?.name, "device-os") + XCTAssertEqual(event.dd.replayStats?.recordsCount, 1) } func testWhenInitialViewHasCconfiguredSource_itSendsViewUpdateEventWithConfiguredSource() throws { @@ -1901,96 +1906,6 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.dd.documentVersion, 1, "It should record only one view update") } - // MARK: Suppressing number of view updates - - // swiftlint:disable opening_brace - func testGivenScopeWithViewUpdatesThrottler_whenReceivingStreamOfCommands_thenItSendsLessViewUpdatesThanScopeWithNoThrottler() throws { - let commandsIssuingViewUpdates: [(Date) -> RUMCommand] = [ - // receive resource: - { date in RUMStartResourceCommand.mockWith(resourceKey: "resource", time: date) }, - { date in RUMStopResourceCommand.mockWith(resourceKey: "resource", time: date) }, - // add action: - { date in RUMStartUserActionCommand.mockWith(time: date, actionType: .scroll) }, - { date in RUMStopUserActionCommand.mockWith(time: date, actionType: .scroll) }, - // add error: - { date in RUMAddCurrentViewErrorCommand.mockWithErrorObject(time: date) }, - // add long task: - { date in RUMAddLongTaskCommand.mockWith(time: date) }, - // receive timing: - { date in RUMAddViewTimingCommand.mockWith(time: date, timingName: .mockRandom()) }, - ] - let stopViewCommand: [(Date) -> RUMCommand] = [ - { date in RUMStopViewCommand.mockWith(time: date, identity: mockView) } - ] - - let commands = (0..<5).flatMap({ _ in commandsIssuingViewUpdates }) + stopViewCommand // loop 5 times through all commands - let timeIntervalBetweenCommands = 1.0 - let simulationDuration = timeIntervalBetweenCommands * Double(commands.count) - let samplingDuration = simulationDuration * 0.5 // at least half view updates should be skipped - - // Given - let throttler = RUMViewUpdatesThrottler(viewUpdateThreshold: samplingDuration) - let sampledWriter = FileWriterMock() - let sampledScope: RUMViewScope = .mockWith( - parent: parent, - dependencies: .mockWith( - viewUpdatesThrottlerFactory: { throttler } - ) - ) - - let noOpThrottler = NoOpRUMViewUpdatesThrottler() - let notSampledWriter = FileWriterMock() - let notSampledScope: RUMViewScope = .mockWith( - parent: parent, - dependencies: .mockWith( - viewUpdatesThrottlerFactory: { noOpThrottler } - ) - ) - - // When - func send(commands: [(Date) -> RUMCommand], to scope: RUMViewScope, writer: Writer) { - var currentTime = scope.viewStartTime - commands.forEach { command in - currentTime.addTimeInterval(timeIntervalBetweenCommands) - _ = scope.process( - command: command(currentTime), - context: context, - writer: writer - ) - } - } - - send(commands: commands, to: sampledScope, writer: sampledWriter) - send(commands: commands, to: notSampledScope, writer: notSampledWriter) - - // Then - let viewUpdatesInSampledScope = sampledWriter.events(ofType: RUMViewEvent.self) - let viewUpdatesInNotSampledScope = notSampledWriter.events(ofType: RUMViewEvent.self) - XCTAssertLessThan( - viewUpdatesInSampledScope.count, - viewUpdatesInNotSampledScope.count , - "Sampled scope should send less view updates than not sampled" - ) - - let actualSamplingRatio = Double(viewUpdatesInSampledScope.count) / Double(viewUpdatesInNotSampledScope.count) - let maxExpectedSamplingRatio = samplingDuration / simulationDuration - XCTAssertLessThan( - actualSamplingRatio, - maxExpectedSamplingRatio, - "Less than \(maxExpectedSamplingRatio * 100)% of view updates should be sampled" - ) - - let actualLastUpdate = try XCTUnwrap(viewUpdatesInSampledScope.last) - let expectedLastUpdate = try XCTUnwrap(viewUpdatesInNotSampledScope.last) - XCTAssertEqual(actualLastUpdate.view.resource.count, expectedLastUpdate.view.resource.count, "It should count all resources") - XCTAssertEqual(actualLastUpdate.view.action.count, expectedLastUpdate.view.action.count, "It should count all actions") - XCTAssertEqual(actualLastUpdate.view.error.count, expectedLastUpdate.view.error.count, "It should count all errors") - XCTAssertEqual(actualLastUpdate.view.longTask?.count, expectedLastUpdate.view.longTask?.count, "It should count all long tasks") - XCTAssertEqual(actualLastUpdate.view.customTimings?.count, expectedLastUpdate.view.customTimings?.count, "It should count all view timings") - XCTAssertTrue(actualLastUpdate.view.isActive == false, "Terminal view update must always be sent") - } - // swiftlint:enable opening_brace - // MARK: Integration with Crash Context func testWhenViewIsStarted_thenItUpdatesLastRUMViewEventInCrashContext() throws { @@ -2034,50 +1949,4 @@ class RUMViewScopeTests: XCTestCase { let rumViewSent = try XCTUnwrap(core.events(ofType: RUMViewEvent.self).last, "It should send view event") DDAssertReflectionEqual(viewEvent, rumViewSent, "It must inject sent event to crash context") } - - // MARK: Cross-platform crashes - - func testGivenScopeWithViewUpdatesThrottler_whenReceivingCrossPlatformCrash_thenItSendsViewUpdateWithUpdatedCrashCount() throws { - let commands: [(Date) -> RUMCommand] = [ - // receive resource: - { date in RUMStartResourceCommand.mockWith(resourceKey: "resource", time: date) }, { date in RUMStopResourceCommand.mockWith(resourceKey: "resource", time: date) }, - // receive error: - { date in RUMAddCurrentViewErrorCommand.mockWithErrorMessage(time: date, attributes: [CrossPlatformAttributes.errorIsCrash: true]) } - ] - let timeIntervalBetweenCommands = 1.0 - let simulationDuration = timeIntervalBetweenCommands * Double(commands.count) - let samplingDuration = simulationDuration * 0.5 // at least half view updates should be skipped - - // Given - let throttler = RUMViewUpdatesThrottler(viewUpdateThreshold: samplingDuration) - let sampledWriter = FileWriterMock() - let sampledScope: RUMViewScope = .mockWith( - parent: parent, - dependencies: .mockWith( - viewUpdatesThrottlerFactory: { throttler } - ) - ) - - // When - func send(commands: [(Date) -> RUMCommand], to scope: RUMViewScope, writer: Writer) { - var currentTime = scope.viewStartTime - commands.forEach { command in - currentTime.addTimeInterval(timeIntervalBetweenCommands) - _ = scope.process( - command: command(currentTime), - context: context, - writer: writer - ) - } - } - - send(commands: commands, to: sampledScope, writer: sampledWriter) - - // Then - let viewUpdatesInSampledScope = sampledWriter.events(ofType: RUMViewEvent.self) - XCTAssertEqual(viewUpdatesInSampledScope.count, 2, "It should send the first event and the error") - - let actualLastUpdate = try XCTUnwrap(viewUpdatesInSampledScope.last) - XCTAssertEqual(actualLastUpdate.view.crash?.count, 1, "It should contain a crash") - } } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/Utils/RUMViewUpdatesThrottlerTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/Utils/RUMViewUpdatesThrottlerTests.swift deleted file mode 100644 index 6a549185a1..0000000000 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/Utils/RUMViewUpdatesThrottlerTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -@testable import Datadog - -class RUMViewUpdatesThrottlerTests: XCTestCase { - private let randomViewUpdateThreshold: TimeInterval = .mockRandom(min: 1, max: 10) - private lazy var throttler = RUMViewUpdatesThrottler(viewUpdateThreshold: randomViewUpdateThreshold) - - func testItAcceptsTheFirstEvent() { - XCTAssertTrue(throttler.accept(event: .mockRandom())) - } - - func testItAcceptsTheLastEvent() { - XCTAssertTrue(throttler.accept(event: .mockRandomWith(viewIsActive: false))) - } - - func testItRejectsNextEventWhenItArrivesEarlierThanThreshold() { - let firstEvent: RUMViewEvent = .mockRandomWith( - viewIsActive: true, // not a final event - viewTimeSpent: 0, - crashCount: 0 - ) - let nextEvent: RUMViewEvent = .mockRandomWith( - viewIsActive: true, // not a final event - viewTimeSpent: randomViewUpdateThreshold.toInt64Nanoseconds - 1, - crashCount: 0 - ) - - XCTAssertTrue(throttler.accept(event: firstEvent), "It should always accepts first event") - XCTAssertFalse(throttler.accept(event: nextEvent), "It should reject next event as it arrived earlier than threshold") - } - - func testItAcceptsNextEventWhenItArrivesLaterThanThreshold() { - let firstEvent: RUMViewEvent = .mockRandomWith( - viewIsActive: true, // not a final event - viewTimeSpent: 0, - crashCount: 0 - ) - let nextEvent: RUMViewEvent = .mockRandomWith( - viewIsActive: true, // not a final event - viewTimeSpent: randomViewUpdateThreshold.toInt64Nanoseconds + 1, - crashCount: 0 - ) - - XCTAssertTrue(throttler.accept(event: firstEvent), "It should always accepts first event") - XCTAssertTrue(throttler.accept(event: nextEvent), "It should accept next event as it arrived later than threshold") - } -} diff --git a/Tests/DatadogTests/Datadog/RUM/RUMViewEventsFilterTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMViewEventsFilterTests.swift new file mode 100644 index 0000000000..4da40e901a --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/RUMViewEventsFilterTests.swift @@ -0,0 +1,123 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +@testable import Datadog + +final class RUMViewEventsFilterTests: XCTestCase { + let sut = RUMViewEventsFilter() + + // MARK: - Base cases + + func testFilterWhenNoEvents() throws { + let events = [Event]() + + let actual = sut.filter(events: events) + let expected = [Event]() + + XCTAssertEqual(actual, expected) + } + + func testFilterWhenNoMetadata() throws { + let events = [ + try Event(data: "A.1", metadata: nil), + try Event(data: "A.2", metadata: nil), + try Event(data: "A.3", metadata: nil), + try Event(data: "A.4", metadata: nil) + ] + + let actual = sut.filter(events: events) + + XCTAssertEqual(actual, events) + } + + func testFilterWhenMixedMissingMetadata() throws { + let events = [ + try Event(data: "A.1", metadata: nil), + try Event(data: "A.2", metadata: nil), + try Event(data: "B.1", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 1)), + try Event(data: "B.2", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 2)), + try Event(data: "C.1", metadata: nil), + try Event(data: "B.3", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 3)), + try Event(data: "A.3", metadata: nil) + ] + + let actual = sut.filter(events: events) + let expected = [ + try Event(data: "A.1", metadata: nil), + try Event(data: "A.2", metadata: nil), + try Event(data: "C.1", metadata: nil), + try Event(data: "B.3", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 3)), + try Event(data: "A.3", metadata: nil) + ] + + XCTAssertEqual(actual, expected) + } + + // MARK: - Common filtering scenarios + + func testFilterWhenSameEvent() throws { + let events = [ + try Event(data: "A.1", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 1)), + try Event(data: "A.2", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 2)), + try Event(data: "A.3", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 3)), + try Event(data: "A.4", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 4)) + ] + + let actual = sut.filter(events: events) + let expected = [ + try Event(data: "A.4", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 4)) + ] + + XCTAssertEqual(actual, expected) + } + + func testFilterWhenMixedEvents() throws { + let events = [ + try Event(data: "B.1", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 1)), + try Event(data: "A.5", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 5)), + try Event(data: "B.2", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 2)), + ] + + let actual = sut.filter(events: events) + let expected = [ + try Event(data: "A.5", metadata: RUMViewEvent.Metadata(id: "A", documentVersion: 5)), + try Event(data: "B.2", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 2)), + ] + + XCTAssertEqual(actual, expected) + } + + func testFilterWhenSingleEvent() throws { + let events = [ + try Event(data: "B.3", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 3)), + ] + + let actual = sut.filter(events: events) + let expected = [ + try Event(data: "B.3", metadata: RUMViewEvent.Metadata(id: "B", documentVersion: 3)), + ] + + XCTAssertEqual(actual, expected) + } +} + +extension Event { + init(data: String, metadata: RUMViewEvent.Metadata?) throws { + self.init(data: data.utf8Data, metadata: try JSONEncoder().encode(metadata)) + } +} + +extension Event: AnyMockable { + public static func mockAny() -> Self { + return mockWith() + } + + public static func mockWith(data: Data = .init(), metadata: Data? = nil) -> Self { + return Event(data: data, metadata: metadata) + } +} diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index 43c9982cd4..b0328b832e 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -32,7 +32,7 @@ class RUMMonitorTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rumFeature - ).replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: rumFeature.configuration.dateProvider ) } diff --git a/Tests/DatadogTests/Datadog/TracerTests.swift b/Tests/DatadogTests/Datadog/TracerTests.swift index e9b405d088..b32281c810 100644 --- a/Tests/DatadogTests/Datadog/TracerTests.swift +++ b/Tests/DatadogTests/Datadog/TracerTests.swift @@ -715,7 +715,7 @@ class TracerTests: XCTestCase { // // // then // let spanMatcher = try core.waitAndReturnSpanMatchers()[0] -// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextAttributes.IDs.sessionID)")) +// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta._dd.\(RUMContextAttributes.IDs.sessionID)")) // } // TODO: RUMM-2843 [V2 regression] RUM context is not associated with span started on caller thread @@ -743,8 +743,8 @@ class TracerTests: XCTestCase { // try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextAttributes.IDs.applicationID)"), // rum.configuration.applicationID // ) -// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextAttributes.IDs.sessionID)")) -// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextAttributes.IDs.viewID)")) +// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta._dd.\(RUMContextAttributes.IDs.sessionID)")) +// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta._dd.\(RUMContextAttributes.IDs.viewID)")) // } // MARK: - Injecting span context into carrier diff --git a/Tests/DatadogTests/DatadogObjc/DDRUMMonitorTests.swift b/Tests/DatadogTests/DatadogObjc/DDRUMMonitorTests.swift index a859b7edf9..d9b8bee805 100644 --- a/Tests/DatadogTests/DatadogObjc/DDRUMMonitorTests.swift +++ b/Tests/DatadogTests/DatadogObjc/DDRUMMonitorTests.swift @@ -153,8 +153,7 @@ class DDRUMMonitorTests: XCTestCase { dependencies: RUMScopeDependencies( core: core, rumFeature: rumFeature - ) - .replacing(viewUpdatesThrottlerFactory: { NoOpRUMViewUpdatesThrottler() }), + ), dateProvider: rumFeature.configuration.dateProvider ) return DatadogObjc.DDRUMMonitor(swiftRUMMonitor: swiftMonitor) diff --git a/tools/rum-models-generator/README.md b/tools/rum-models-generator/README.md index 457e5b40c4..37b86535a0 100644 --- a/tools/rum-models-generator/README.md +++ b/tools/rum-models-generator/README.md @@ -4,10 +4,10 @@ ## Usage -To update the data models to the latest version: - +To update the data models to the latest version, call this in the root directory of the `dd-sdk-ios`. ``` -# make rum-models-generator +# make rum-models-generate +# make sr-models-generate ``` ## License diff --git a/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift b/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift index 381610826b..def1aaff88 100644 --- a/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift +++ b/tools/rum-models-generator/Sources/CodeDecoration/RUMCodeDecorator.swift @@ -22,7 +22,6 @@ public class RUMCodeDecorator: SwiftCodeDecorator { "RUMCITest", "RUMDevice", "RUMOperatingSystem", - "RUMDisplay", "RUMActionID", ] ) @@ -101,10 +100,6 @@ public class RUMCodeDecorator: SwiftCodeDecorator { fixedName = "RUMOperatingSystem" } - if fixedName == "Display" { - fixedName = "RUMDisplay" - } - // Since https://github.com/DataDog/rum-events-format/pull/57 `action.id` can be either // single `String` or an array of `[String]`. This is handled by generating Swift enum with // two cases and different associated types. To not duplicate generated code in each nested diff --git a/tools/rum-models-generator/Sources/CodeDecoration/SRCodeDecorator.swift b/tools/rum-models-generator/Sources/CodeDecoration/SRCodeDecorator.swift index b5c7eafd31..2b60dff8c6 100644 --- a/tools/rum-models-generator/Sources/CodeDecoration/SRCodeDecorator.swift +++ b/tools/rum-models-generator/Sources/CodeDecoration/SRCodeDecorator.swift @@ -21,6 +21,7 @@ public class SRCodeDecorator: SwiftCodeDecorator { "SRShapeWireframe", "SRTextWireframe", "SRImageWireframe", + "SRPlaceholderWireframe", // For convenience, make fat `*Record` structures to be root types: "SRFullSnapshotRecord", "SRIncrementalSnapshotRecord", diff --git a/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift b/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift index e589d9848c..efddd5b759 100644 --- a/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift +++ b/tools/rum-models-generator/Sources/CodeGeneration/Generate/JSONSchema.swift @@ -288,3 +288,29 @@ internal class JSONSchema: Decodable { } } } + +extension Array where Element == JSONSchema.EnumValue { + func inferrSchemaType() -> JSONSchema.SchemaType? { + let hasOnlyStrings = allSatisfy { element in + if case .string = element { + return true + } + return false + } + if hasOnlyStrings { + return .string + } + + let hasOnlyIntegers = allSatisfy { element in + if case .integer = element { + return true + } + return false + } + if hasOnlyIntegers { + return .number + } + + return nil + } +} diff --git a/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/JSON/JSONSchemaToJSONTypeTransformer.swift b/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/JSON/JSONSchemaToJSONTypeTransformer.swift index e03be2f3f5..e248acfd82 100644 --- a/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/JSON/JSONSchemaToJSONTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/JSON/JSONSchemaToJSONTypeTransformer.swift @@ -35,8 +35,14 @@ internal class JSONSchemaToJSONTypeTransformer { return try transformSchemaToObject(schema, named: name) } - let schemaType = try schema.type - .unwrapOrThrow(.inconsistency("`JSONSchema` must define `type`: \(schema).")) + let schemaType: JSONSchema.SchemaType + if let enumarations = schema.enum, schema.type == nil { + schemaType = try enumarations.inferrSchemaType() + .unwrapOrThrow(.inconsistency("Heteregenous enum is not supported: \(enumarations).")) + } else { + schemaType = try schema.type + .unwrapOrThrow(.inconsistency("`JSONSchema` must define `type`: \(schema).")) + } switch schemaType { case .object: diff --git a/tools/rum-models-generator/Tests/CodeGenerationTests/Fixtures/fixture-schema-with-typeless-enum.json b/tools/rum-models-generator/Tests/CodeGenerationTests/Fixtures/fixture-schema-with-typeless-enum.json new file mode 100644 index 0000000000..0413fa6a7a --- /dev/null +++ b/tools/rum-models-generator/Tests/CodeGenerationTests/Fixtures/fixture-schema-with-typeless-enum.json @@ -0,0 +1,16 @@ +{ + "$id": "Schema ID", + "type": "object", + "title": "Schema title", + "description": "Schema description.", + "properties": { + "stringEnumProperty": { + "description": "Description of `stringEnumProperty` without explicit type.", + "enum": ["case1", "case2", "case3", "case4"] + }, + "integerEnumProperty": { + "description": "Description of `integerEnumProperty` without explicit type.", + "enum": [1, 2, 3, 4] + } + } +} diff --git a/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONSchemaToJSONTypeTransformerTests.swift b/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONSchemaToJSONTypeTransformerTests.swift index 96e2b9825f..db9b384e57 100644 --- a/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONSchemaToJSONTypeTransformerTests.swift +++ b/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONSchemaToJSONTypeTransformerTests.swift @@ -215,4 +215,44 @@ final class JSONSchemaToJSONTypeTransformerTests: XCTestCase { let actual = try JSONSchemaToJSONTypeTransformer().transform(jsonSchema: jsonSchema) XCTAssertEqual(expected, actual as? JSONUnionType) } + + func testTransformingJSONSchemaWithNoExplicitEnumTypeIntoJSONObject() throws { + let expected = JSONObject( + name: "Schema title", + comment: "Schema description.", + properties: [ + JSONObject.Property( + name: "stringEnumProperty", + comment: "Description of `stringEnumProperty` without explicit type.", + type: JSONEnumeration( + name: "stringEnumProperty", + comment: "Description of `stringEnumProperty` without explicit type.", + values: [.string(value: "case1"), .string(value: "case2"), .string(value: "case3"), .string(value: "case4")] + ), + defaultValue: nil, + isRequired: false, + isReadOnly: true + ), + JSONObject.Property( + name: "integerEnumProperty", + comment: "Description of `integerEnumProperty` without explicit type.", + type: JSONEnumeration( + name: "integerEnumProperty", + comment: "Description of `integerEnumProperty` without explicit type.", + values: [.integer(value: 1), .integer(value: 2), .integer(value: 3), .integer(value: 4)] + ), + defaultValue: nil, + isRequired: false, + isReadOnly: true + ) + ] + ) + + let file = Bundle.module.url(forResource: "Fixtures/fixture-schema-with-typeless-enum", withExtension: "json")! + + let jsonSchema = try JSONSchemaReader().read(file) + + let actual = try JSONSchemaToJSONTypeTransformer().transform(jsonSchema: jsonSchema) + XCTAssertEqual(expected, actual as? JSONObject) + } } diff --git a/tools/rum-models-generator/run.py b/tools/rum-models-generator/run.py index 5fe6c3926c..fb4514c6dd 100755 --- a/tools/rum-models-generator/run.py +++ b/tools/rum-models-generator/run.py @@ -24,7 +24,7 @@ # Generated file paths (relative to repository root) RUM_SWIFT_GENERATED_FILE_PATH = '/Sources/Datadog/RUM/DataModels/RUMDataModels.swift' RUM_OBJC_GENERATED_FILE_PATH = '/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift' -SR_SWIFT_GENERATED_FILE_PATH = '/DatadogSessionReplay/Sources/DatadogSessionReplay/Writer/Models/SRDataModels.swift' +SR_SWIFT_GENERATED_FILE_PATH = '/DatadogSessionReplay/Sources/Writer/Models/SRDataModels.swift' @dataclass diff --git a/tools/sr-snapshots/Sources/Shell/CommandLine.swift b/tools/sr-snapshots/Sources/Shell/CommandLine.swift index 18bea92162..4d66aba4eb 100644 --- a/tools/sr-snapshots/Sources/Shell/CommandLine.swift +++ b/tools/sr-snapshots/Sources/Shell/CommandLine.swift @@ -6,8 +6,10 @@ import Foundation -public struct CommandLineError: Error, CustomStringConvertible { - let result: CommandLineResult +/// An error thrown when shell command exited with non-zero code. +public struct CommandError: Error, CustomStringConvertible { + /// The full result of command. + let result: CommandResult public var description: String { return """ @@ -19,10 +21,10 @@ public struct CommandLineError: Error, CustomStringConvertible { } /// Result of executing shell command. -public struct CommandLineResult { - /// Command's STDOUT value. +public struct CommandResult { + /// Command's STDOUT. public let output: String? - /// Command's STDERR value. + /// Command's STDERR. public let error: String? /// Exit code of the command. public let status: Int32 @@ -31,9 +33,9 @@ public struct CommandLineResult { /// Protocol for running command line commands public protocol CommandLine { /// Executes given shell command. - /// - Parameter command: command to run - /// - Returns: result of the command - func shellResult(_ command: String) throws -> CommandLineResult + /// - Parameter command: The command to run. + /// - Returns: The result of the command. + func shellResult(_ command: String) throws -> CommandResult } public extension CommandLine { @@ -45,7 +47,7 @@ public extension CommandLine { func shell(_ command: String) throws -> String { let result = try shellResult(command) if result.status != 0 { - throw CommandLineError(result: result) + throw CommandError(result: result) } else if let output = result.output, !output.isEmpty { return output } else if let error = result.error, !error.isEmpty { diff --git a/tools/sr-snapshots/Sources/Shell/ProcessCommandLine.swift b/tools/sr-snapshots/Sources/Shell/ProcessCommandLine.swift index 0b15ae8ba8..bd0a6cf395 100644 --- a/tools/sr-snapshots/Sources/Shell/ProcessCommandLine.swift +++ b/tools/sr-snapshots/Sources/Shell/ProcessCommandLine.swift @@ -5,96 +5,138 @@ */ import Foundation +import Dispatch +/// Runs a child process with capturing standard output and standard error. +/// +/// Inspired by https://developer.apple.com/forums/thread/690310 public class ProcessCommandLine: CommandLine { - private var output: [String] = [] - private var error: [String] = [] + public init() {} - private let notificationCenter: NotificationCenter - - public init(notificationCenter: NotificationCenter = .default) { - self.notificationCenter = notificationCenter - } - - @discardableResult - public func shellResult(_ command: String) throws -> CommandLineResult { - output = [] - error = [] + /// Executes given shell command. + /// - Parameter command: The command to run. + /// - Returns: The result of the command. + public func shellResult(_ command: String) throws -> CommandResult { + var result: Result? = nil + let queue = DispatchQueue(label: "com.datadoghq.cli-\(UUID().uuidString)") print("🐚 β†’ \(command)") - let outpipe = Pipe() - let outfh = outpipe.fileHandleForReading - outfh.waitForDataInBackgroundAndNotify() - notificationCenter.addObserver(self, selector: #selector(readOutput), name: NSNotification.Name.NSFileHandleDataAvailable, object: outfh) - - let errpipe = Pipe() - let errfh = errpipe.fileHandleForReading - errfh.waitForDataInBackgroundAndNotify() - notificationCenter.addObserver(self, selector: #selector(readError), name: NSNotification.Name.NSFileHandleDataAvailable, object: errfh) - - let task = Process() - task.launchPath = "/bin/bash" - task.arguments = ["-c", command] - task.standardOutput = outpipe - task.standardError = errpipe - task.launch() - task.waitUntilExit() - - if task.isRunning { - fatalError() + let semaphore = DispatchSemaphore(value: 0) + shellWithCompletion(command, on: queue) { callbackResult in + result = callbackResult + semaphore.signal() } + _ = semaphore.wait(timeout: .distantFuture) - return CommandLineResult( - output: output.joined(separator: "\n"), - error: error.joined(separator: "\n"), - status: task.terminationStatus - ) - } - - @objc - private func readOutput(notification: Notification) { - guard let handle = notification.object as? FileHandle else { - return + switch result! { // swiftlint:disable:this force_unwrapping + case .success(let result): return result + case .failure(let error): throw error } + } - let data = handle.availableData - if let str = String(data: data, encoding: .utf8) { - if let sanitized = sanitize(line: str) { - print(sanitized) - output.append(sanitized) + internal func shellWithCompletion( + _ command: String, + on queue: DispatchQueue, + completion: @escaping (Result) -> Void + ) { + queue.async { + let processGroup = DispatchGroup() + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + var stdoutData = Data() + var stderrData = Data() + var posixError: Error? = nil + + let task = Process() + task.launchPath = "/bin/bash" + task.arguments = ["-c", command] + task.standardOutput = stdoutPipe + task.standardError = stderrPipe + + processGroup.enter() + task.terminationHandler = { _ in + // In case the latter `try task.run()` throws, bouncing the group leave() + // on queue here ensures it is properly teared down. + queue.async { processGroup.leave() } } - } - - handle.waitForDataInBackgroundAndNotify() - } - @objc - private func readError(notification: Notification) { - guard let handle = notification.object as? FileHandle else { - return - } + // This runs the supplied block when all three events have completed (task + // termination and the end of both STDOUT and STDERR reads). + processGroup.notify(queue: queue) { + if let error = posixError { + completion(.failure(error)) + } else { + let result = CommandResult( + stdoutData: stdoutData, + stderrData: stderrData, + terminationStatus: task.terminationStatus + ) + completion(.success(result)) + } + } - let data = handle.availableData - if let str = String(data: data, encoding: .utf8) { - if let sanitized = sanitize(line: str) { - print(sanitized) - error.append(sanitized) + do { + func posixErr(_ error: Int32) -> Error { + NSError(domain: NSPOSIXErrorDomain, code: Int(error), userInfo: nil) + } + + try task.run() + + // Enter the process group and leaver it only after STDOUT buffer is read. + processGroup.enter() + let stdoutFile = stdoutPipe.fileHandleForReading + let stdoutReadIO = DispatchIO(type: .stream, fileDescriptor: stdoutFile.fileDescriptor, queue: queue) { _ in + try! stdoutFile.close() // swiftlint:disable:this force_try + } + stdoutReadIO.read(offset: 0, length: .max, queue: queue) { isDone, chunk, error in + stdoutData.append(contentsOf: chunk ?? .empty) + if isDone || error != 0 { + stdoutReadIO.close() + if posixError == nil && error != 0 { posixError = posixErr(error) } + processGroup.leave() + } + } + + // Enter the process group and leaver it only after STDERR buffer is read. + processGroup.enter() + let stderrFile = stderrPipe.fileHandleForReading + let stderrReadIO = DispatchIO(type: .stream, fileDescriptor: stderrFile.fileDescriptor, queue: queue) { _ in + try! stderrFile.close() // swiftlint:disable:this force_try + } + stderrReadIO.read(offset: 0, length: .max, queue: queue) { isDone, chunk, error in + stderrData.append(contentsOf: chunk ?? .empty) + if isDone || error != 0 { + stderrReadIO.close() + if posixError == nil && error != 0 { posixError = posixErr(error) } + processGroup.leave() + } + } + } catch { + posixError = error + // We’ve only entered the group once at this point, so the single leave done by the + // termination handler is enough to run the notify block and call the + // client’s completion handler. + task.terminationHandler!(task) // swiftlint:disable:this force_unwrapping } } - handle.waitForDataInBackgroundAndNotify() } +} - /// Removes new lines and trailing spaces from a string - private func sanitize(line: String) -> String? { - var trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) - guard trimmed.count > 0 else { - return nil - } - - // replace tabs with space - trimmed = trimmed.replacingOccurrences(of: "\t", with: " ") +private extension CommandResult { + init(stdoutData: Data, stderrData: Data, terminationStatus: Int32) { + self.output = String(data: stdoutData, encoding: .utf8).flatMap(sanitize(output:)) + self.error = String(data: stderrData, encoding: .utf8).flatMap(sanitize(output:)) + self.status = terminationStatus + } +} - return trimmed +/// Removes new lines and trailing spaces from a string +private func sanitize(output: String) -> String? { + var trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines) + guard trimmed.count > 0 else { + return nil } + trimmed = trimmed.replacingOccurrences(of: "\t", with: " ") + return trimmed } diff --git a/tools/sr-snapshots/Tests/ShellTests/ProcessCommandLineTests.swift b/tools/sr-snapshots/Tests/ShellTests/ProcessCommandLineTests.swift index 78e9eda3f1..49e446a94f 100644 --- a/tools/sr-snapshots/Tests/ShellTests/ProcessCommandLineTests.swift +++ b/tools/sr-snapshots/Tests/ShellTests/ProcessCommandLineTests.swift @@ -10,23 +10,40 @@ import XCTest class ShellCommandTests: XCTestCase { private let cli = ProcessCommandLine() - func testWhenCommandExitsWithCode0_itReturnsOutput() throws { - let output = try cli.shell("echo 'foo bar' && exit 0") - XCTAssertEqual(output, "foo bar") + func testWhenCommandPrintsToStandardOutputAndExitsWith0() throws { + let output = try cli.shell("echo 'STDOUT foo' && exit 0") + XCTAssertEqual(output, "STDOUT foo") } - func testWhenCommandExitsWithCodeOtherThan0_itThrowsErrorAndReturnsOutput() throws { - XCTAssertThrowsError(try cli.shell("echo 'foo bar' && exit 1")) { error in + func testWhenCommandPrintsToStandardErrorAndExitsWith0() throws { + let output = try cli.shell("echo 'STDERR foo' 1>&2 && exit 0") + XCTAssertEqual(output, "STDERR foo") + } + + func testWhenCommandPrintsToBothOutputsAndExitsWith0() throws { + let output = try cli.shell("echo 'STDERR foo' 1>&2 && echo 'STDOUT foo' && exit 0") + XCTAssertEqual(output, "STDOUT foo") + } + + func testWhenCommandExitsWithOtherCode() throws { + XCTAssertThrowsError(try cli.shell("echo 'STDERR foo' 1>&2 && echo 'STDOUT foo' && exit 1")) { error in // swiftlint:disable trailing_whitespace XCTAssertEqual( - (error as? CommandLineError)?.description, + (error as? CommandError)?.description, """ status: 1 - output: foo bar - error: + output: STDOUT foo + error: STDERR foo """ ) // swiftlint:enable trailing_whitespace } } + + func testCallingMultipleCommandsFromDifferentThreads() throws { + DispatchQueue.concurrentPerform(iterations: 100) { _ in + let output = try? cli.shell("echo 'STDOUT foo' && exit 0") + XCTAssertEqual(output, "STDOUT foo") + } + } }